Troubleshooting AD Group Policy That Does Not Apply
Troubleshooting AD Group Policy That Does Not Apply
A successful gpupdate with no expected setting is not one diagnosis. The GPO may be outside scope, unreadable, disabled for the relevant section, or fully applied while a particular preference item is legitimately skipped. Rejoining the domain or broadening permissions at the first missing value often changes the wrong layer.
The examples use Windows Server 2022 Datacenter hosts LAB-DC1 and LAB-MEMBER in corp.test.checo.cc with Windows PowerShell 5.1. Work through permissions, backup restoration, WMI filtering, disabled sections, and preference targeting using dedicated OUs, GPOs, and registry markers.
Start on the actual affected client. Use an elevated session for computer policy; inspect user policy in the intended user’s real session, not another administrator’s HKCU:
$t = Get-Date
whoami /user
gpupdate /target:computer /force /wait:120
$updateExit = $LASTEXITCODE
gpresult /scope computer /r
$resultExit = $LASTEXITCODE
"GPUpdateExit=$updateExit; GPResultExit=$resultExit"
Get-WinEvent -FilterHashtable @{
LogName='Microsoft-Windows-GroupPolicy/Operational'
StartTime=$t
} | Select-Object TimeCreated,Id,RecordId,Message/wait limits command waiting. If output says background processing continues, wait for that cycle to finish before deciding or starting another refresh. The report, events, and effective setting need the same observation window.
1. Locate the failure
| Observation | First checks |
|---|---|
| Entire GPO absent from user report | Object location, links, user authorization, computer retrieval rights |
| Denied (Security) | Effective Read/Apply, explicit denies, current group identity |
| Denied (WMI Filter) | Complete associated WQL and actual client properties |
| Link exists, Not Applied (Unknown Reason) | GpoStatus, enabled sections, and correlated events |
| Applied GPO, only some items missing | Preference action, disabled state, item targeting, target path |
These are entry points, not unique error-to-root-cause mappings. Both missing Read and missing Apply produced Denied (Security) in the computer experiment. The user GPO with no computer Read did not even appear as an explicitly denied GPO.
2. Security filtering
The permission experiment granted GpoApply to KB225-Computers while retaining GpoRead for Authenticated Users. This separated retrieval from application.
| Computer condition | Report | ComputerMarker |
|---|---|---|
| Not in target group | Denied (Security) | ABSENT |
| Added, only gpupdate run | Still denied; new group absent from report | ABSENT |
| Restarted after addition, then refreshed | Group and GPO present | ReadApplyOK |
| Same group downgraded from Apply to Read | Denied (Security) | ABSENT |
Neither the link nor policy contents changed in the final comparisons. Updating directory membership and refreshing the client’s current identity are distinct from editing the GPO ACL. Restart supplied a repeatable positive control here; repeatedly refreshing policy was not treated as rebuilding identity.
On the management machine, select the intended GPO/domain/DC and preserve its backup, report, permissions, and OU links. This block diagnoses existing lab objects; it does not create the lab:
$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
if ($d.DNSRoot -ne $domain) { throw 'Domain mismatch' }
$gpoName = 'KB225-ComputerFilter'
$g = Get-GPO -Name $gpoName -Domain $domain -Server $dc
$c = Get-ADComputer LAB-MEMBER -Server $dc -Properties memberOf
$c | Select-Object DistinguishedName,ObjectGUID,memberOf
$backup = 'C:\GPO-Permission-Backup\' + (Get-Date -Format yyyyMMdd-HHmmss)
New-Item -ItemType Directory -Path $backup | Out-Null
$b = Backup-GPO -Guid $g.Id -Path $backup -Domain $domain -Server $dc
$b | Export-Clixml ($backup + '\backup-instance.xml')
Get-GPOReport -Guid $g.Id -ReportType Xml -Path ($backup + '\gpo-before.xml') -Domain $domain -Server $dc
Get-GPPermission -Guid $g.Id -All -Domain $domain -Server $dc |
Select-Object @{Name='Trustee';Expression={$_.Trustee.Name}},Permission
Get-ADOrganizationalUnit -Filter * -Server $dc -Properties gPLink,gPOptions |
Select-Object DistinguishedName,ObjectGUID,gPLink,gPOptions |
Export-Clixml ($backup + '\links-before.xml')Grant target-group Apply while reducing Authenticated Users to Read:
Set-GPPermission -Guid $g.Id -TargetName 'KB225-Computers' -TargetType Group -PermissionLevel GpoApply -Domain $domain -Server $dc
Set-GPPermission -Guid $g.Id -TargetName 'Authenticated Users' -TargetType Group -PermissionLevel GpoRead -Replace -Domain $domain -Server $dc
Get-GPPermission -Guid $g.Id -All -Domain $domain -Server $dc |
Select-Object @{Name='Trustee';Expression={$_.Trustee.Name}},@{Name='Permission';Expression={$_.Permission.ToString()}}-Replace matters when reducing an existing higher permission. Read the ACL back; successful execution alone does not establish that Apply disappeared. Do not repair limited targeting by granting everyone Apply again.
On the member, refresh and read the actual value:
gpupdate /target:computer /force /wait:120
$updateExit = $LASTEXITCODE
gpresult /scope computer /r
$resultExit = $LASTEXITCODE
Get-ItemProperty 'HKLM:\Software\Policies\KB225' -Name ComputerMarker
"GPUpdateExit=$updateExit; GPResultExit=$resultExit"The read-only negative control used:
Set-GPPermission -Guid $g.Id -TargetName 'KB225-Computers' -TargetType Group -PermissionLevel GpoRead -Replace -Domain $domain -Server $dc
Get-GPPermission -Guid $g.Id -All -Domain $domain -Server $dcRestore the intended Apply grant after that test. In the read-only phase, both commands returned zero and the secure channel remained True, yet the marker was missing. Neither exit codes nor domain trust health proves that a particular GPO applied.
User Apply does not replace computer Read
A second GPO granted ordinary user KB225-User Apply but had no retrieval permission for the member after Authenticated Users was removed. In the real user session, the applied list was N/A and UserMarker=ABSENT. Operational event 5312 listed None; 5311 reported no loopback mode.
Keep the user rights and session unchanged, and grant the computer group Read only:
$ug = Get-GPO -Name 'KB225-UserRead' -Domain $domain -Server $dc
$ubackup = 'C:\GPO-Permission-Backup\User-' + (Get-Date -Format yyyyMMdd-HHmmss)
New-Item -ItemType Directory -Path $ubackup | Out-Null
$ub = Backup-GPO -Guid $ug.Id -Path $ubackup -Domain $domain -Server $dc
$ub | Export-Clixml ($ubackup + '\backup-instance.xml')
Set-GPPermission -Guid $ug.Id -TargetName 'KB225-Computers' -TargetType Group -PermissionLevel GpoRead -Domain $domain -Server $dc
Get-GPPermission -Guid $ug.Id -All -Domain $domain -Server $dc |
Select-Object @{Name='Trustee';Expression={$_.Trustee.Name}},@{Name='Permission';Expression={$_.Permission.ToString()}}Run this in the same ordinary user’s window:
whoami /user
whoami /groups
gpupdate /target:user /force /wait:120
$updateExit = $LASTEXITCODE
gpresult /scope user /r
$resultExit = $LASTEXITCODE
gpresult /scope user /h "$env:TEMP\KB225-user-result.html" /f
Get-ItemProperty 'HKCU:\Software\Policies\KB225' -Name UserMarker
"GPUpdateExit=$updateExit; GPResultExit=$resultExit"The GPO appeared, UserMarker=UserReadOK, and both command exit codes were zero. Event 4017 began reading its gpt.ini. The computer retrieves user policy following MS16-072; this is separately documented in Microsoft’s retrieval-permission guidance.
Add Read through GPMC Delegation without expanding Apply targeting. A DC logon can mask this problem through Enterprise Domain Controllers’ existing Read permission, which is why the test used an ordinary member and nonadministrator user.
3. Restore and scope
The restore experiment used HKLM\Software\Policies\KB08Restore\Marker. Its baseline was Healthy-v1; the broken control changed it to Broken-v2, reduced Authenticated Users to Read, and removed the OU link.
| Phase | GPO content | Permission | OU link | Client marker |
|---|---|---|---|---|
| Healthy | Healthy-v1 | Apply | Enabled | Healthy-v1 |
| Broken control | Broken-v2 | Read | Absent | Absent |
| After Restore | Healthy-v1 | Apply | Still absent | Absent |
| Recreate original link | Healthy-v1 | Apply | Enabled | Healthy-v1 |
The directory policy container and SYSVOL template hold the policy. OU gPLink and gPOptions hold links/inheritance. Preserve them separately.
In backup output, GpoId identifies the GPO; Id identifies that specific backup. One GPO can have many backup instances. Use BackupId when choosing a historical version rather than accidentally selecting the newest broken backup:
GpoId : 3811e645-2285-4528-9403-f67af2c12b2e
Id : 9e9eabdc-1c35-4777-8597-46595197d27eIn this recovery example, $gpoId is the confirmed affected GUID, $ou is its target OU DN, and $root is the new backup directory. Preserve the current version before restoring:
Import-Module ActiveDirectory,GroupPolicy
$domain = 'corp.test.checo.cc'
$dc = 'LAB-DC1.corp.test.checo.cc'
$gpoId = [guid](Read-Host 'Affected GPO GUID')
$ou = Read-Host 'Target OU DN'
$root = 'C:\GpoRecovery-' + (Get-Date -Format yyyyMMdd-HHmmss)
New-Item -ItemType Directory -Path $root -ErrorAction Stop | Out-Null
$goodPath = Read-Host 'Known-good GPO backup root directory'
$backupId = [guid](Read-Host 'Known-good backup BackupId')
$currentPath = Join-Path $root 'before-restore'
New-Item -Path $currentPath -ItemType Directory -ErrorAction Stop | Out-Null
Backup-GPO -Guid $gpoId -Path $currentPath -Domain $domain -Server $dc | Out-Null
Restore-GPO -BackupId $backupId -Path $goodPath -Domain $domain -Server $dc -Confirm:$false
Get-GPO -Guid $gpoId -Domain $domain -Server $dc | Format-List
Get-GPPermission -Guid $gpoId -All -Domain $domain -Server $dc | Format-Table
$links = (Get-GPInheritance -Target $ou -Domain $domain -Server $dc).GpoLinks
$links | Format-Table DisplayName,GpoId,Enabled,Enforced,OrderOnly recreate a missing link when the original link record establishes it should exist. Restore Enforced and Order from that record rather than applying this lab’s values everywhere:
$existing = @($links | Where-Object GpoId -eq $gpoId)
if ($existing.Count -eq 0) {
New-GPLink -Guid $gpoId -Target $ou -LinkEnabled Yes -Enforced No -Order 1 -Domain $domain -Server $dc
}
Get-ADOrganizationalUnit -Identity $ou -Server $dc -Properties gPLink,gPOptions |
Format-List DistinguishedName,gPLink,gPOptionsThe restored GPO retained its GUID but advanced to user version 1 and computer version 3. AD versionNumber and GPT.INI both contained 65539. Recovery is not a demand to roll version counters back to their old numbers.
Import behaved differently: importing the healthy source backup into an existing target replaced settings while keeping the target GUID, Read-only authorization, and disabled link. Enabling the link exposed Denied (Security); only restoring the target’s intended Apply permission made Healthy-v1 arrive. Restoring the original object and importing into a different one are different operations. See Restore-GPO.
4. WMI filtering
The WMI experiment changed only ProductType=3 to ProductType=1. GPO, link, ACL, and content remained the same. LAB-MEMBER returned one object for the first query and none for the second:
| Condition | Local objects | Policy result | Marker |
|---|---|---|---|
| ProductType=3 | 1 | Applied | WmiApplied |
| ProductType=1 | 0 | Denied (WMI Filter) | ABSENT |
| Restore ProductType=3 | 1 | Applied | WmiApplied |
ProductType distinguishes client (1), DC (2), and non-DC server (3), not Standard versus Datacenter editions. The Server 2022 machine’s WMI Version was 10.0.20348 and BuildNumber was string 20348. Deriving a version filter from a marketing name can exclude the intended machine.
Collect full WQL, local results, and the refresh’s policy events on the member, not a management workstation or DC:
$ErrorActionPreference='Stop'
$start=Get-Date
$os=Get-CimInstance Win32_OperatingSystem
$good='SELECT * FROM Win32_OperatingSystem WHERE ProductType=3'
$bad='SELECT * FROM Win32_OperatingSystem WHERE ProductType=1'
$goodCount=@(Get-CimInstance -Namespace root\CIMv2 -Query $good).Count
$badCount=@(Get-CimInstance -Namespace root\CIMv2 -Query $bad).Count
$update=gpupdate /target:computer /force /wait:120 2>&1
$updateExit=$LASTEXITCODE
$result=gpresult /scope computer /z 2>&1
$resultExit=$LASTEXITCODE
if($updateExit -ne 0 -or $resultExit -ne 0){throw "GP command failed $updateExit $resultExit"}
$key='HKLM:\Software\Policies\KBEAD141011'
$marker='ABSENT'
if(Test-Path $key){$item=Get-Item $key;if($item.GetValueNames() -contains 'Marker'){$marker=$item.GetValue('Marker')}}
@{Start=$start.ToUniversalTime().ToString('o');End=(Get-Date).ToUniversalTime().ToString('o');OS=$os.Caption;Version=$os.Version;ProductType=$os.ProductType;GoodQueryCount=$goodCount;BadQueryCount=$badCount;GPUpdateExit=$updateExit;GPResultExit=$resultExit;Marker=$marker;Update=@($update);Result=@($result);Events=@(Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-GroupPolicy/Operational';StartTime=$start} -ErrorAction SilentlyContinue|Select-Object TimeCreated,Id,RecordId,Message)}|ConvertTo-Json -Depth 6Export the complete filter definition, namespace, all queries, and associations before editing. A filter can serve several GPOs. Use a separate filter when their intended scopes differ rather than editing a shared definition or removing filtering wholesale. Afterward, verify the definition and association on the DC actually used by the client; account for propagation rather than mixing versions across DCs.
5. Disabled sections
The section-state experiment kept the policy file hash, ACL, and links unchanged while varying GpoStatus:
| GpoStatus | flags | User section | Computer section |
|---|---|---|---|
| AllSettingsEnabled | 0 | Enabled | Enabled |
| UserSettingsDisabled | 1 | Skipped | Enabled |
| ComputerSettingsDisabled | 2 | Enabled | Skipped |
| AllSettingsDisabled | 3 | Skipped | Skipped |
The healthy marker was Baseline19. Disabling the computer section removed it and yielded Not Applied (Unknown Reason). Disabling only the user section let the computer marker return. Re-enabling and changing the content to ReEnabled19 delivered the new value, ruling out a stale leftover as the explanation.
Read status, GPC flags, version, extension registration, ACL, and SYSVOL file on the same DC:
$ErrorActionPreference='Stop';$ProgressPreference='SilentlyContinue';
Import-Module ActiveDirectory,GroupPolicy; $dc='LAB-DC1.corp.test.checo.cc'; $domain='corp.test.checo.cc'; $root='DC=corp,DC=test,DC=checo,DC=cc';
$g=Get-GPO -Guid '7d35103b-a5e9-42ec-bb02-68184da5f13c' -Domain $domain -Server $dc
$g=Get-GPO -Guid $g.Id -Domain $domain -Server $dc
$a=Get-ADObject -Identity ("CN={"+$g.Id+"},CN=Policies,CN=System,$root") -Server $dc -Properties flags,versionNumber,gPCMachineExtensionNames,nTSecurityDescriptor
$ou=Get-ADObject -Identity '9bee8030-26d1-4790-a365-e4802f5b579e' -Server $dc -Properties gPLink
$p="\\$dc\SYSVOL\$domain\Policies\{"+$g.Id+"}\Machine\Registry.pol"
@{status=$g.GpoStatus.ToString();flags=$a.flags;version=$a.versionNumber;cse=$a.gPCMachineExtensionNames;sddl=$a.nTSecurityDescriptor.Sddl;link=$ou.gPLink;pol_hash=(Get-FileHash $p).Hash;wmi=if($g.WmiFilter){$g.WmiFilter.Name}else{$null}} | ConvertTo-Json -Depth 6The GUIDs identify historical lab objects; select current affected GUIDs before reusing the diagnostic. After backup and recording the original GpoStatus, change it through GPMC Details or the management object:
$ErrorActionPreference='Stop';$ProgressPreference='SilentlyContinue';
Import-Module ActiveDirectory,GroupPolicy; $dc='LAB-DC1.corp.test.checo.cc'; $domain='corp.test.checo.cc'; $root='DC=corp,DC=test,DC=checo,DC=cc';
$g=Get-GPO -Guid '7d35103b-a5e9-42ec-bb02-68184da5f13c' -Domain $domain -Server $dc
$g.GpoStatus='AllSettingsEnabled'
$g=Get-GPO -Guid $g.Id -Domain $domain -Server $dcDo not hand-edit GPC flags or SYSVOL files. A deliberately computer-only GPO can retain UserSettingsDisabled. Select the state that matches its design, read it back, and inspect the original client’s effective value and same-cycle event list.
6. Preference targeting
The GPP experiment placed two Update items in one GPO. MarkerNoTarget=gpp-baseline had no condition; the other item required NetBIOS computer name NOTLABMEMBER. The client was LAB-MEMBER.
The GPO appeared in Applied and the extension finished, but only the unconditional item wrote a value. Its target XML was:
<Filters>
<FilterComputer bool="AND" not="0" type="NETBIOS" name="NOTLABMEMBER" />
</Filters>Changing only the name to LAB-MEMBER made both values appear. False targeting is normal filtering, not necessarily an execution error. Expand the complete AND/OR/negation/collection tree before changing a single condition.
| Phase | Unconditional item | Targeted item | Extension result |
|---|---|---|---|
| Name mismatch | gpp-baseline | Absent | Completed, 234 ms |
| Name matches | gpp-baseline | gpp-badtarget | Completed, 62 ms |
The two 4016/5016 pairs were records 6659/6660 and 6697/6698. The message named Group Policy Registry Extension for preferences; another Registry Extension record belongs to a different path. Extension completion does not prove every preference item executed.
Keep client, action, and path unchanged and repair the intended condition. GPO WMI filtering controls the entire GPO; preference targeting controls the item. An individual item failure does not justify removing the GPO’s security filtering.
7. Validation and recovery
Keep identity/DN, contacted DC, GPO GUID/version, RSoP, same-window Operational events, and the real registry/file/service result. Inspect user policy as the user and computer policy in its proper context. A report name or zero exit code is not the business outcome.
Each independent experiment preserved GPO backups and original links. Reverse only the actual change: original ACL, exported WMI definition/association, GpoStatus, or the specific link. Do not overwrite an entire OU’s old gPLink string and erase someone else’s concurrent additions.
For disposable labs, remove links, refresh and verify that settings leave scope, return the original computer object, then remove only recorded test GUIDs and empty OUs. Registry policy markers disappeared on refresh in these tests. GPP Update values can remain; precisely remove test values that were originally absent after policy leaves scope, or restore a pre-existing value’s content, type, and existence.
After cleanup, check computer identity/location, the secure channel, SYSVOL/NETLOGON, and the default GPO baseline. These checks confirm recovery of the test environment; investigate any remaining domain-health errors separately.
