Windows Server for beginners. Part 24: How does Group Policy work?

All parts of the series “Windows Server for beginners”

Group Policy has come up in almost every part: in part 3 the client downloaded it from SYSVOL at logon, in part 12 it was supposed to configure time on the PDC, in part 20 its backups were on the list of mandatory ones, in part 23 it was the suggested way to fill local groups. Time to take it apart: what a policy object is at the level of the directory and files, how the client decides which policies apply to it and in what order, and how to manage all of it from the GroupPolicy module — and at the end, build that very policy for the PDC, entirely with commands.

All commands below run from the administrator's workstation ws1 with RSAT (part 15) in PowerShell 7. The domain is corp.example.com, with controllers dc1 and dc2 in the site Office, dc3 and dc4 in the site Branch; dc1 is the PDC. The GroupPolicy module doesn't declare compatibility with PowerShell 7 and by default loads through the compatibility layer: objects arrive deserialized, and nested properties like $_.Computer.DSVersion come out empty in calculated columns. So we load the module directly:

PS C:\> Import-Module GroupPolicy -SkipEditionCheck

What a policy consists of

A Group Policy Object (GPO) lives in two places at once, and that explains half of its oddities.

The container in the directory (GPC, Group Policy Container) is an object of class groupPolicyContainer named by a GUID in CN=Policies,CN=System:

PS C:\> Get-ADObject 'CN={31B2F340-016D-11D2-945F-00C04FB984F9},CN=Policies,CN=System,DC=corp,DC=example,DC=com' -Properties displayName, versionNumber, gPCFileSysPath, gPCMachineExtensionNames, gPCUserExtensionNames, flags |
    Format-List displayName, Name, versionNumber, flags, gPCFileSysPath, gPCMachineExtensionNames, gPCUserExtensionNames

displayName              : Default Domain Policy
Name                     : {31B2F340-016D-11D2-945F-00C04FB984F9}
versionNumber            : 76
flags                    : 0
gPCFileSysPath           : \\corp.example.com\sysvol\corp.example.com\Policies\{31B2F340-016D-11D2-945F-00C04FB984F9}
gPCMachineExtensionNames : [{35378EAC-683F-11D2-A89A-00C04FBBCFA2}{53D6AB1B-2488-11D1-A28C-00C04FB94F17}{D02B1F72-3407-48AE-BA88-E8213C6761F1}][{827D319E-6EAC-11D2-A4EA-00C04F79F83A}{803E14A0-B4FB-11D0-A0D0-00A0C90F574B}][{B1BE8D72-6EAC-11D2-A4EA-00C04F79F83A}{53D6AB1B-2488-11D1-A28C-00C04FB94F17}]
gPCUserExtensionNames    :

The name, version, path to the files, flags and the list of client-side extensions (CSE) that must process this policy. Each pair of square brackets is one extension: first the GUID of the extension itself, then one or more GUIDs of the editor components the settings were written with. Here there are three extensions: registry {35378EAC-…} (administrative templates and public key policies), security settings {827D319E-…} and the EFS recovery agent {B1BE8D72-…}. If an extension isn't on the list, the client won't look for its settings in the files even if they are there. This policy has no user part — gPCUserExtensionNames is empty.

flags are the disabled halves of the policy: 0 — everything enabled, 1 — user part disabled, 2 — computer part, 3 — both. The module shows the same as the GpoStatus property (AllSettingsEnabled, UserSettingsDisabled, ComputerSettingsDisabled, AllSettingsDisabled).

The folder in SYSVOL (GPT, Group Policy Template), at the path from gPCFileSysPath:

\\corp.example.com\SYSVOL\corp.example.com\Policies\{GUID}\
    GPT.INI                              [General] Version=…
    GPO.cmt                              policy comment (Unicode)
    Machine\
        Registry.pol                     registry settings for the computer (administrative templates)
        Microsoft\Windows NT\SecEdit\GptTmpl.inf    security settings: passwords, Kerberos, rights, audit
        Scripts\Startup\, Scripts\Shutdown\
        Preferences\                     preferences, a folder per type: Drives, Registry, Groups, ScheduledTasks …
    User\
        Registry.pol
        Scripts\Logon\, Scripts\Logoff\
        Preferences\

Folders appear only when something is written into them: a policy with one registry setting for the computer has just GPT.INI and Machine\Registry.pol. In the two policies created with the domain, the folders are named MACHINE and USER in capitals — SYSVOL is case-insensitive.

The version is a 32-bit number: the high 16 bits are the version of the user part, the low 16 bits — of the computer part. Each save of a part increments its counter by one. In the directory it's stored in versionNumber, in SYSVOL — in GPT.INI:

PS C:\> Get-ADObject -SearchBase 'CN=Policies,CN=System,DC=corp,DC=example,DC=com' -SearchScope OneLevel -Filter * -Properties displayName, versionNumber, flags |
    Format-Table displayName, versionNumber, @{n='User'; e={$_.versionNumber -shr 16}}, @{n='Computer'; e={$_.versionNumber -band 0xFFFF}}, flags -AutoSize

displayName                       versionNumber User Computer flags
-----------                       ------------- ---- -------- -----
Default Domain Policy                        76    0       76     0
Users - Intranet Zone                    196608    3        0     0
Network - Prefer IPv4                         8    0        8     0
Network - Prefer IPv6                     65539    1        3     0
Network Discovery                            74    0       74     0
Default Domain Controllers Policy            76    0       76     0
Remote Desktop                               20    0       20     0
Time - No Hyper-V Sync                        1    0        1     0
Workstations - Guest SMB                      5    0        5     0
DC - PDC Time Source                         10    0       10     0

196608 = 0x00030000: the user part was saved three times, the computer part never. 65539 = 0x00010003: once and three times. The module shows both halves and the version in SYSVOL:

PS C:\> Get-GPO -All | Format-Table DisplayName, GpoStatus, @{n='User'; e={$_.User.DSVersion}}, @{n='Computer'; e={$_.Computer.DSVersion}}, @{n='Sysvol'; e={$_.Computer.SysvolVersion}}, @{n='WmiFilter'; e={$_.WmiFilter.Name}} -AutoSize

DisplayName                                GpoStatus User Computer Sysvol WmiFilter
-----------                                --------- ---- -------- ------ ---------
Default Domain Policy             AllSettingsEnabled    0       76     76
Users - Intranet Zone             AllSettingsEnabled    3        0      0
Network - Prefer IPv4             AllSettingsEnabled    0        8      8
Network - Prefer IPv6             AllSettingsEnabled    1        3      3
Network Discovery                 AllSettingsEnabled    0       74     74
Default Domain Controllers Policy AllSettingsEnabled    0       76     76
Remote Desktop                    AllSettingsEnabled    0       20     20
Time - No Hyper-V Sync            AllSettingsEnabled    0        1      1
Workstations - Guest SMB          AllSettingsEnabled    0        5      5
DC - PDC Time Source              AllSettingsEnabled    0       10     10 PDC emulator

The container replicates with the directory, and the folder replicates by DFSR with SYSVOL (part 10), and these are two independent mechanisms. The client decides whether a policy has changed by the container, and reads the settings from the files. If on the controller the client contacted the directory is already updated but SYSVOL isn't yet, the client gets the old files or doesn't find the new ones — hence events 1058 on freshly created policies. The gpresult /h report marks such a policy "AD / SYSVOL Version Mismatch", and the GPMC console shows the discrepancies on the infrastructure status tab.

Two objects with fixed GUIDs are created when the domain is installed; they are the same in every domain. Default Domain Policy, as Microsoft recommends, holds only the account policies: passwords, lockout and Kerberos. For domain accounts these settings take effect only from policies linked to the domain root (set in an OU policy, they affect only the local accounts of that OU's machines), and the controllers carry them over into attributes of the domain root (minPwdLength, lockoutThreshold and others from part 5); separate password requirements for groups are set not by a policy but by PSO objects (fine-grained password policy) in the directory. Default Domain Controllers Policy holds user rights and audit on the controllers; by default it also turns on mandatory SMB and secure channel signing (RequireSecuritySignature, RequireSignOrSeal in GptTmpl.inf). Nothing else goes into them: your own settings go into your own policies, and if these two get damaged, dcgpofix /target:domain or /target:dc returns them to their original state.


Linking

A policy on its own applies to no one. It gets linked to a site, domain or OU, and the link is the gPLink attribute on that object:

PS C:\> Get-ADObject 'DC=corp,DC=example,DC=com' -Properties gPLink, gPOptions | Format-List gPLink, gPOptions

gPLink    : [LDAP://cn={8b9c0d1e-2f3a-4b4c-9d5e-6f7a8b9c0d1e},cn=policies,cn=system,DC=corp,DC=example,DC=com;0][LDAP://cn={3C5D6E7F-8091-4A2B-B3C4-D5E6F7081920},cn=policies,cn=system,DC=corp,DC=example,DC=com;0][LDAP://CN={31B2F340-016D-11D2-945F-00C04FB984F9},CN=Policies,CN=System,DC=corp,DC=example,DC=com;0]
gPOptions :

In the schema gPLink is a single string, but Group Policy reads it as a list: in square brackets the DN of the policy container and, after a semicolon, the link flags — 0 normal, 1 disabled, 2 enforced, 3 — disabled enforced. A new link is written at the beginning of the string, and they are applied right to left: the last one in the string comes first in order (Order 1) and wins over its neighbours in the container. The same domain as the module sees it:

PS C:\> (Get-GPInheritance -Target 'DC=corp,DC=example,DC=com').GpoLinks | Format-Table Order, DisplayName, Enabled, Enforced -AutoSize

Order DisplayName            Enabled Enforced
----- -----------            ------- --------
    1 Default Domain Policy     True    False
    2 Users - Intranet Zone     True    False
    3 Time - No Hyper-V Sync    True    False

Default Domain Policy, linked first, is last in the string and has order 1; those linked later got 2 and 3. gPOptions = 1 on a domain or OU means block inheritance; an empty attribute means 0. If you remove the last link, gPLink is left holding a string of one space.

The client gathers policies along the LSDOU chain: the machine's local policy, then the policies of the site, the domain, the OU and nested OUs down to the one where the object lives. They are applied in order, and each next one overrides the previous one in the settings it defines. Hence the rules:

  • a setting from an OU beats a setting from the domain, from a nested OU — from the parent;
  • block inheritance on an OU cuts off everything above — except enforced links;
  • an enforced link beats everything below it, including blocking, and of two enforced links the higher one wins;
  • within one container the link with the lower number wins.
PS C:\> (Get-GPInheritance -Target 'OU=Domain Controllers,DC=corp,DC=example,DC=com').InheritedGpoLinks | Format-Table Order, DisplayName, Enabled, Enforced, Target -AutoSize

Order DisplayName                       Enabled Enforced Target
----- -----------                       ------- -------- ------
    1 Default Domain Controllers Policy    True    False OU=Domain Controllers,DC=corp,DC=example,DC=com
    2 Network Discovery                    True    False OU=Domain Controllers,DC=corp,DC=example,DC=com
    3 Remote Desktop                       True    False OU=Domain Controllers,DC=corp,DC=example,DC=com
    4 DC - PDC Time Source                 True    False OU=Domain Controllers,DC=corp,DC=example,DC=com
    1 Default Domain Policy                True    False DC=corp,DC=example,DC=com
    2 Users - Intranet Zone                True    False DC=corp,DC=example,DC=com
    3 Time - No Hyper-V Sync               True    False DC=corp,DC=example,DC=com

The list goes in descending precedence: the top row beats all those below. Order is the number of the link within its own container, so the domain's policies start numbering again. The module puts enforced links at the top of the list, and block inheritance on the OU (GpoInheritanceBlocked : Yes in the output of Get-GPInheritance) removes everything above from it, except them.

A special mode is loopback: normally user settings come from the policies linked to the user's OU; on terminal servers and kiosks you want them to come from the computer's OU. The setting "Configure user Group Policy loopback processing mode" (Computer Configuration → Administrative Templates → System → Group Policy) in Replace mode takes only the computer's policies for the user, in Merge mode — appends them after the user's, and on conflict they win. The client records the mode in the log with event 5311.


How the client applies them

The Group Policy Client service (gpsvc) on every machine in the domain:

  1. At boot — computer policies, at user logon — that user's policies; then in the background every 90 minutes with a random offset of up to 30 (on controllers — every 5). Since Windows 8 and Server 2012 the background refresh is started by a scheduled task, and judging by the log the offset is chosen once when the service starts: on a workstation the background refresh events come exactly 105 minutes apart, after a reboot — exactly 106.
  2. Finds a controller (event 5308 in the log) and asks it over LDAP for gPLink and gPOptions of its site, its domain and the chain of OUs down to its object; for each policy found — its container: version, flags, extension list, permissions, WMI filter.
  3. Drops the disabled ones, those that failed the filters, and those empty for this half (version 0); an extension whose policies and policy list haven't changed doesn't reapply its settings.
  4. For the rest, reads files from SYSVOL over SMB — at the path from gPCFileSysPath, through the DFS namespace \\corp.example.com\SYSVOL, which hands out referrals to controllers starting with its own site.
  5. Passes each extension on the list its files, and the extensions apply the settings: registry writes to HKLM\SOFTWARE\Policies and HKCU\SOFTWARE\Policies, security settings are applied by the secedit engine, preferences create drives and tasks.

Not everything is applied in the background: software installation only at boot or logon, folder redirection at logon, scripts run at startup, shutdown, logon and logoff. If such an extension has something to apply, it writes event 1112 (a restart or a new logon is needed), and gpupdate offers to restart or log off. On a slow link (below 500 kbps by default, event 5314 with the estimate) registry and security are always applied, while software installation, folder redirection and scripts are not. Since Windows 8.1 and Server 2012 R2 the client saves the policies it received to a local store (event 5216), and with synchronous processing at the next boot reads them from there rather than from a controller; this is controlled by the "Configure Group Policy Caching" setting.

A word on tattooing. Administrative template settings are written to the Policies registry branches that applications read first; when the policy stops applying, the client deletes those keys and the setting returns to its default. Preferences and some security settings write to ordinary keys, and after the policy is removed the value stays — a "tattoo". It's not a bug but a property to keep in mind when removing a policy.


Kinds of settings

Section Where it's stored What
Administrative templates Registry.pol thousands of Windows and application settings described by ADMX files; the result is keys under Policies
Security settings GptTmpl.inf account policies (effective only from policies at the domain root), user rights, audit, restricted groups, services, ACLs on the registry and files, firewall, AppLocker
Scripts Scripts\ run at startup, shutdown, logon, logoff; PowerShell or anything
Software installation the directory and an MSI on a share assigning and publishing MSIs; aging, but works
Folder redirection the directory Documents and Desktop to a server
Preferences (Group Policy Preferences, GPP) Preferences\*.xml network drives, printers, shortcuts, files, registry keys, local users and groups, scheduled tasks, environment variables — with item-level targeting by dozens of conditions (group, OS, IP, time, file existence) and without tattooing if you check "Remove this item when it is no longer applied"

Preferences are also recorded in gPCMachineExtensionNames: a policy with a registry preference has [{B087BE9D-…}{BEE07A6A-…}] there — the Group Policy Registry extension and its editor component. The editor also writes an entry with a zero extension GUID, [{00000000-0000-0000-0000-000000000000}{BEE07A6A-…}], listing its components — policies built with commands may not have it.

Administrative templates are described by ADMX files (structure) and ADML files (language strings, a folder per language: en-US, ru-RU), and every Windows version has its own. The editor takes them from the local machine unless the domain has a Central Store — a PolicyDefinitions folder in SYSVOL that all editors in the domain read from. A fresh domain doesn't have one:

PS C:\> $store = '\\corp.example.com\SYSVOL\corp.example.com\Policies\PolicyDefinitions'
PS C:\> Test-Path $store
False
PS C:\> (Get-ChildItem C:\Windows\PolicyDefinitions -Filter *.admx).Count
225

Windows 11 25H2 has 225 templates locally. The store is created by copying the whole C:\Windows\PolicyDefinitions folder from the newest Windows in the domain (or from the Administrative Templates package Microsoft releases for each version):

PS C:\> Copy-Item C:\Windows\PolicyDefinitions -Destination $store -Recurse

For updates Microsoft advises building a new folder under another name (PolicyDefinitions-25H2), adding the vendors' templates for Office, Edge, Chrome to it, and only then renaming it to PolicyDefinitions, keeping the old one for rollback: files of different versions copied over each other cause errors in the editor.


Filters

The link to an OU is the first filter. The second is permissions: a policy is applied only by those who have Read and Apply Group Policy on its container. By default that's Authenticated Users:

PS C:\> Get-GPPermission -Name 'DC - PDC Time Source' -All | Format-Table @{n='Trustee'; e={$_.Trustee.Name}}, @{n='Type'; e={$_.Trustee.SidType}}, Permission, Denied, Inherited -AutoSize

Trustee                                 Type                  Permission Denied Inherited
-------                                 ----                  ---------- ------ ---------
Authenticated Users           WellKnownGroup                    GpoApply  False     False
Domain Admins                          Group GpoEditDeleteModifySecurity  False     False
Enterprise Admins                      Group GpoEditDeleteModifySecurity  False     False
ENTERPRISE DOMAIN CONTROLLERS WellKnownGroup                     GpoRead  False     False
SYSTEM                        WellKnownGroup GpoEditDeleteModifySecurity  False     False

To make a policy apply to one group, Apply is taken from Authenticated Users and given to that group. Since the 2016 security update (MS16-072) the computer reads user policies with its own account, and if Read is taken from Authenticated Users as well, user policies stop applying altogether. So Authenticated Users is lowered to read rather than removed (or Read is given to Domain Computers). Without -Replace, Set-GPPermission doesn't lower a higher level — it silently keeps the old one:

PS C:\> Set-GPPermission -Name 'Sales - Drive Maps' -TargetName 'Authenticated Users' -TargetType Group -PermissionLevel GpoRead -Replace
PS C:\> Set-GPPermission -Name 'Sales - Drive Maps' -TargetName 'Sales' -TargetType Group -PermissionLevel GpoApply

Both calls return the policy object. Without -Server the module's cmdlets contact the PDC.

The third is a WMI filter: a WQL query against the machine, and the policy applies if it returned anything. The classics: SELECT * FROM Win32_OperatingSystem WHERE ProductType = 2 — domain controllers; SELECT * FROM Win32_ComputerSystem WHERE DomainRole = 5 — the PDC owner; SELECT * FROM Win32_OperatingSystem WHERE Version LIKE "10.0.2%" — by version. The query runs on the client at every processing, and a slow query slows down boot. The filter is an object of class msWMI-Som in CN=SOM,CN=WMIPolicy,CN=System, and the gPCWQLFilter attribute of the policy container ties it to the policy; a policy has at most one filter, one filter can serve any number of policies. The module has no cmdlet for creating filters — only the snap-in or writing to the directory directly, as in the example below. Preferences have their own, more convenient mechanism — item-level targeting — and where possible, filter with that.


The GroupPolicy module

Commands for everything above:

Get-GPO -All; Get-GPO -Name 'Servers - Baseline'                 # objects
New-GPO -Name 'X' -Comment '…'; Remove-GPO; Rename-GPO; Copy-GPO   # creation
New-GPLink -Name 'X' -Target 'OU=Servers,DC=corp,DC=example,DC=com' -LinkEnabled Yes -Order 1 -Enforced No
Set-GPLink -Name 'X' -Target '…' -Enforced Yes; Remove-GPLink
Get-GPInheritance -Target '…'; Set-GPInheritance -Target '…' -IsBlocked Yes
Set-GPRegistryValue -Name 'X' -Key 'HKLM\SOFTWARE\Policies\…' -ValueName 'V' -Type DWord -Value 1   # administrative templates by key
Get-GPRegistryValue -Name 'X' -Key 'HKLM\SOFTWARE\Policies\…'
Set-GPPrefRegistryValue -Name 'X' -Context Computer -Action Update -Key '…' -ValueName 'V' -Type String -Value 'a'   # registry preference
Set-GPPermission -Name 'X' -TargetName 'Helpdesk' -TargetType Group -PermissionLevel GpoEdit
Get-GPOReport -Name 'X' -ReportType Html -Path .\X.html; Get-GPOReport -All -ReportType Xml
Backup-GPO -All -Path D:\GPOBackup; Restore-GPO -Name 'X' -Path D:\GPOBackup
Import-GPO -BackupGpoName 'X' -Path D:\GPOBackup -TargetName 'X' -CreateIfNeeded -MigrationTable .\map.migtable   # into another domain
Get-GPResultantSetOfPolicy -Computer ws1 -User CORP\alice -ReportType Html -Path .\rsop.html
Invoke-GPUpdate -Computer ws1 -RandomDelayInMinutes 0 -Force       # remote gpupdate through a scheduled task (since 2012)

Invoke-GPUpdate doesn't connect to the machine over WinRM: it creates a scheduled task on it that runs gpupdate. For that the machine must have the Remote Scheduled Tasks Management (RPC and RPC-EPMAP) and Windows Management Instrumentation (WMI-In) rules open; on servers they are disabled by default (part 15), and then Invoke-Command dc1 { gpupdate /target:computer } is simpler. Per the documentation, the cmdlet's -Force only suppresses the confirmation prompt.

Get-GPOReport -ReportType Xml returns the whole policy: name, creation and modification time, security descriptor, WMI filter name and description, versions of both parts (VersionDirectory and VersionSysvol), for each administrative template setting — its name in the editor, state and help text, and at the end — where the policy is linked (LinksTo).

What the module lacks: security settings (GptTmpl.inf is a text INI, edited in the editor or copied from a reference policy with Import-GPO), scripts, preferences other than registry (their XML can be put into the policy folder by hand with the extension GUID added to gPCMachineExtensionNames, but that's internals), WMI filters.


A policy for the PDC

As promised in part 12: a time source that follows the PDC role. Three parts — a WMI filter for the role, a policy with W32Time settings, a link to the controllers' OU. First the filter — an object in the directory, no cmdlet:

PS C:\> $domain = Get-ADDomain
PS C:\> $id = '{' + (New-Guid).Guid.ToUpper() + '}'
PS C:\> $now = (Get-Date).ToUniversalTime().ToString('yyyyMMddHHmmss.ffffff') + '-000'
PS C:\> $query = 'SELECT * FROM Win32_ComputerSystem WHERE DomainRole = 5'
PS C:\> $attrs = @{
    'msWMI-Name'             = 'PDC emulator'
    'msWMI-Parm1'            = 'Domain controller holding the PDC emulator role (DomainRole = 5)'
    'msWMI-Parm2'            = "1;3;10;$($query.Length);WQL;root\CIMv2;$query;"
    'msWMI-ID'               = $id
    'msWMI-Author'           = 'alice.admin@corp.example.com'
    'msWMI-CreationDate'     = $now
    'msWMI-ChangeDate'       = $now
    'instanceType'           = 4
    'showInAdvancedViewOnly' = $true
}
PS C:\> New-ADObject -Name $id -Type 'msWMI-Som' -Path "CN=SOM,CN=WMIPolicy,CN=System,$($domain.DistinguishedName)" -OtherAttributes $attrs -PassThru

msWMI-Parm2 is a string with the format version, the number of queries, the query length, the language, the namespace and the query itself. Then the policy: ten values under Policies\Microsoft\W32Time — exactly what the administrative templates Configure Windows NTP Client, Enable Windows NTP Client, Enable Windows NTP Server and AnnounceFlags from Global Configuration Settings write:

PS C:\> $gpo = New-GPO -Name 'DC - PDC Time Source' -Comment 'PDC emulator of the forest root syncs from pool.ntp.org and announces itself as a reliable time source; WMI filter "PDC emulator" makes the setting follow the role.'
PS C:\> $w = 'HKLM\SOFTWARE\Policies\Microsoft\W32Time'
PS C:\> $values = @(
    @{ Key = "$w\Parameters"; ValueName = 'NtpServer'; Type = 'String'; Value = '0.pool.ntp.org,0x8 1.pool.ntp.org,0x8 2.pool.ntp.org,0x8 3.pool.ntp.org,0x8' }
    @{ Key = "$w\Parameters"; ValueName = 'Type'; Type = 'String'; Value = 'NTP' }
    @{ Key = "$w\Config"; ValueName = 'AnnounceFlags'; Type = 'DWord'; Value = 5 }
    @{ Key = "$w\TimeProviders\NtpClient"; ValueName = 'Enabled'; Type = 'DWord'; Value = 1 }
    @{ Key = "$w\TimeProviders\NtpClient"; ValueName = 'CrossSiteSyncFlags'; Type = 'DWord'; Value = 2 }
    @{ Key = "$w\TimeProviders\NtpClient"; ValueName = 'ResolvePeerBackoffMinutes'; Type = 'DWord'; Value = 15 }
    @{ Key = "$w\TimeProviders\NtpClient"; ValueName = 'ResolvePeerBackoffMaxTimes'; Type = 'DWord'; Value = 7 }
    @{ Key = "$w\TimeProviders\NtpClient"; ValueName = 'SpecialPollInterval'; Type = 'DWord'; Value = 1024 }
    @{ Key = "$w\TimeProviders\NtpClient"; ValueName = 'EventLogFlags'; Type = 'DWord'; Value = 0 }
    @{ Key = "$w\TimeProviders\NtpServer"; ValueName = 'Enabled'; Type = 'DWord'; Value = 1 }
)
PS C:\> foreach ($v in $values) { Set-GPRegistryValue -Guid $gpo.Id @v | Select-Object DisplayName, @{n='Computer'; e={$_.Computer.DSVersion}} }

Each call is one save of the computer part, so after ten the version becomes 10. The keys under Policies\Microsoft\W32Time are the same parameters w32tm /config wrote to Services\W32Time, but set by policy: they take precedence and disappear when the policy stops applying. Get-GPOReport shows such values as settings of the corresponding templates rather than as "extra" registry values.

The tie to the filter and the link:

PS C:\> Set-ADObject "CN=$($gpo.Id.ToString('B').ToUpper()),CN=Policies,CN=System,$($domain.DistinguishedName)" -Replace @{ gPCWQLFilter = "[$($domain.DNSRoot);$id;0]" }
PS C:\> New-GPLink -Guid $gpo.Id -Target "OU=Domain Controllers,$($domain.DistinguishedName)" -LinkEnabled Yes
PS C:\> Get-GPO -Guid $gpo.Id | Select-Object DisplayName, @{n='WmiFilter'; e={$_.WmiFilter.Name}}, GpoStatus

DisplayName          WmiFilter             GpoStatus
-----------          ---------             ---------
DC - PDC Time Source PDC emulator AllSettingsEnabled

Without the calculated column, Select-Object WmiFilter shows the type name Microsoft.GroupPolicy.WmiFilter in a table rather than the filter: the filter object has the properties Name, Description and Path. The filter itself in the directory:

PS C:\> Get-ADObject -SearchBase 'CN=SOM,CN=WMIPolicy,CN=System,DC=corp,DC=example,DC=com' -Filter 'objectClass -eq "msWMI-Som"' -Properties msWMI-Name, msWMI-Parm2, msWMI-CreationDate |
    Format-List msWMI-Name, msWMI-Parm2, msWMI-CreationDate

msWMI-Name         : PDC emulator
msWMI-Parm2        : 1;3;10;55;WQL;root\CIMv2;SELECT * FROM Win32_ComputerSystem WHERE DomainRole = 5;
msWMI-CreationDate : 20261025082704.247764-000

On dc2, which has DomainRole = 4 (a controller without the PDC role), the filter returns an empty result, the policy doesn't apply, and it stays in the domain hierarchy. When the role moves, at the next policy refresh (five minutes on controllers) the new PDC gets the external source, while the Policies keys are removed from the old one and it returns to its local settings. That's why the local W32Time configuration on the controllers themselves is left at the default (NT5DS): then no second policy for the other controllers is needed. To check the result on the PDC — Invoke-Command dc1 { w32tm /query /source } after a policy refresh (Invoke-Command dc1 { gpupdate /target:computer }).

A backup after a change is the habit from part 20:

PS C:\> Backup-GPO -Guid $gpo.Id -Path D:\GPOBackup -Comment 'PDC time source' | Select-Object DisplayName, GpoId, Id, BackupDirectory

gpupdate and gpresult

On the client: gpupdate applies the changed policies, gpupdate /force — all of them, /target:computer or /target:user — only one part, /boot and /logoff — with a restart or logoff if the extensions require it, /wait:<seconds> — how long to wait (600 by default). Refreshing only your own user part works without elevation:

PS C:\> gpupdate /target:user
Updating policy...

User Policy update has completed successfully.

What ended up applied is gpresult. Without elevation it sees only the user part; gpresult /r without /scope silently shows just that, while gpresult /r /scope:computer answers "ERROR: Access Denied." — the computer part is viewed from a console run as administrator. The user part on ws1 (the beginning of gpresult /r /scope:user output, without the banner and the group list):

RSOP data for CORP\alice on WS1 : Logging Mode
---------------------------------------------------------------

OS Configuration:            Member Workstation
OS Version:                  10.0.26200
Site Name:                   N/A
Roaming Profile:             N/A
Local Profile:               C:\Users\alice
Connected over a slow link?: No


USER SETTINGS
--------------
    CN=Alice Smith,OU=Sales,OU=People,DC=corp,DC=example,DC=com
    Last time Group Policy was applied: 29.10.2026 at 15:41:11
    Group Policy was applied from:      dc1.corp.example.com
    Group Policy slow link threshold:   500 kbps
    Domain Name:                        CORP
    Domain Type:                        Windows 2008 or later

    Applied Group Policy Objects
    -----------------------------
        Users - Intranet Zone

    The following GPOs were not applied because they were filtered out
    -------------------------------------------------------------------
        Local Group Policy
            Filtering:  Not Applied (Empty)

Default Domain Policy and Time - No Hyper-V Sync are also linked to the domain, but their user part is empty, and they don't appear in this list at all. Further down the output comes the user's group list, against which the permissions on the policies were checked. In the computer part the same report lists the policies dropped by filters with the reason: Denied (Security) — no permission, Denied (WMI Filter) — the filter returned an empty result, Disabled (Link) — the link is disabled.

gpresult /h report.html gives the same report with each setting and the policy it came from — the main tool for "why does this machine have this setting"; gpresult /x gives the same in XML. The file path for both switches is at most 127 characters, otherwise "ERROR: Value for '…' option cannot be more than 127 character(s).". Get-GPResultantSetOfPolicy builds the report for a remote machine and needs the Remote Event Log Management and WMI-In rules open on it.

The client writes the outcome of each processing to the System log: 1500 and 1501 — computer and user policies processed, no changes; 1502 and 1503 — new settings from N policies detected and applied. The detailed record of each cycle is in the Microsoft-Windows-GroupPolicy/Operational log, which can be read only with elevation:

Events What
4000, 4001 / 8000, 8001 start and end of processing at boot and at logon
4004, 4005 / 8004, 8005 manual processing (gpupdate) of the computer and the user
4006, 4007 / 8006, 8007 background processing
5308, 5326 which controller was found and how long it took
5311, 5314 loopback mode, link estimate and slow link threshold
5312, 5313 list of applicable policies and of those filtered out
4016 / 5016, 7016 start of an extension's processing with its policy list, its successful or failed end with the time
5315 when the next processing will happen

Errors in the System log: 1058 — failed to read a policy's gpt.ini from SYSVOL (name resolution and connectivity to the controller, SYSVOL replication latency, a disabled DFS client — the text itself lists these causes); 1030 — failed to retrieve new settings, the error code is on the details tab, usually paired with 1058; 1129 — no connectivity to a controller; 1053 and 1055 — couldn't resolve the user or computer name (name resolution on the controller, replication latency, the secure channel); 1006 — LDAP bind to the controller failed; 1096 — failed to apply a specific policy's Registry.pol. A real 1058 on a workstation:

PS C:\> (Get-WinEvent -FilterHashtable @{LogName='System'; ProviderName='Microsoft-Windows-GroupPolicy'; Id=1058} -MaxEvents 1).Message
The processing of Group Policy failed. Windows attempted to read the file \\corp.example.com\SysVol\corp.example.com\Policies\{3C5D6E7F-8091-4A2B-B3C4-D5E6F7081920}\gpt.ini from a domain controller and was not successful. Group Policy settings may not be applied until this event is resolved. This issue may be transient and could be caused by one or more of the following:
a) Name Resolution/Network Connectivity to the current domain controller.
b) File Replication Service Latency (a file created on another domain controller has not replicated to the current domain controller).
c) The Distributed File System (DFS) client has been disabled.

Isolated 1058s on a fresh policy are usually the SYSVOL of a controller the change hasn't reached yet, and gpresult will tell you which one in the "applied from" line. It also happens that 1058s pour in every five minutes on a controller itself while SYSVOL and DNS are healthy: that's how a virtual controller behaves after its network adapter is reconnected on the hypervisor, and a reboot cures it. The Operational log is small and on a controller wraps in less than a day — investigate a failure right away.


Hands-on

The PDC policy from the example, built in the domain with the same commands. Its files in SYSVOL are only what something was written into:

PS C:\> $g = Get-GPO -Name 'DC - PDC Time Source'
PS C:\> $root = "\\corp.example.com\SYSVOL\corp.example.com\Policies\$($g.Id.ToString('B').ToUpper())"
PS C:\> Get-ChildItem $root -Recurse -File | Select-Object @{n='File'; e={$_.FullName.Substring($root.Length + 1)}}, Length

File                 Length
----                 ------
GPO.cmt                 328
GPT.INI                  23
Machine\Registry.pol   1816

PS C:\> Get-Content "$root\GPT.INI"
[General]
Version=10

PS C:\> $g.Computer | Select-Object DSVersion, SysvolVersion, Enabled

DSVersion SysvolVersion Enabled
--------- ------------- -------
       10            10    True

PS C:\> $g | Select-Object CreationTime, ModificationTime

CreationTime        ModificationTime
------------        ----------------
25.10.2026 11:27:21 25.10.2026 11:27:34

Ten saves in 13 seconds — version 10. GPO.cmt is the comment from New-GPO -Comment; the module shows it as the Description property. The version in the directory and in GPT.INI on each controller — if it's lower somewhere, the change hasn't reached it yet, and clients that contact that controller will apply the policy later:

PS C:\> $id = $g.Id.ToString('B').ToUpper()
PS C:\> 'dc1','dc2','dc3','dc4' | ForEach-Object {
    [pscustomobject]@{
        DC     = $_
        DS     = (Get-ADObject "CN=$id,CN=Policies,CN=System,DC=corp,DC=example,DC=com" -Server $_ -Properties versionNumber).versionNumber
        Sysvol = (Select-String -Path "\\$_\SYSVOL\corp.example.com\Policies\$id\GPT.INI" -Pattern '^Version=(\d+)').Matches.Groups[1].Value
    }
} | Format-Table -AutoSize

DC  DS Sysvol
--  -- ------
dc1 10 10
dc2 10 10
dc3 10 10
dc4 10 10

What's inside Registry.pol — through the module, which parses it:

PS C:\> Get-GPRegistryValue -Name 'DC - PDC Time Source' -Key 'HKLM\SOFTWARE\Policies\Microsoft\W32Time\Parameters' | Format-Table ValueName, Type, Value -AutoSize

ValueName   Type Value
---------   ---- -----
NtpServer String 0.pool.ntp.org,0x8 1.pool.ntp.org,0x8 2.pool.ntp.org,0x8 3.pool.ntp.org,0x8
Type      String NTP

Which containers the client went through for the user and in what order is in the gpresult XML report. That's the LSDOU chain, recorded by the client itself:

PS C:\Temp> gpresult /scope:user /x rsop.xml /f
PS C:\Temp> $x = [xml](Get-Content .\rsop.xml)
PS C:\Temp> $x.Rsop.UserResults.SearchedSOM | Sort-Object { [int]$_.Order } | Format-Table Order, Type, Path, BlocksInheritance

Order Type   Path                                        BlocksInheritance
----- ----   ----                                        -----------------
1     Local  Local                                       false
2     Site   corp.example.com/Configuration/Sites/Office false
3     Domain corp.example.com                            false
4     OU     corp.example.com/People                     false
5     OU     corp.example.com/People/Sales               false

PS C:\Temp> $x.Rsop.UserResults.ExtensionStatus | Format-Table Name, BeginTime, EndTime, Error -AutoSize

Name                        BeginTime           EndTime             Error
----                        ---------           -------             -----
Group Policy Infrastructure 2026-10-29T12:41:11 2026-10-29T12:41:11 0
Registry                    2026-10-18T15:33:42 2026-10-18T15:33:42 0

The infrastructure just ran (times in UTC), while the registry extension last applied settings on October 18 — when the only user policy changed. Since then neither the policies nor their list have changed, and the extension skips every cycle. The computer's background cycles in the System log:

PS C:\> Get-WinEvent -FilterHashtable @{LogName='System'; ProviderName='Microsoft-Windows-GroupPolicy'; Id=1500} -MaxEvents 6 | Format-Table TimeCreated, Id

TimeCreated           Id
-----------           --
29.10.2026 14:24:05 1500
29.10.2026 12:38:06 1500
29.10.2026 10:52:06 1500
29.10.2026 9:06:05  1500
29.10.2026 7:20:05  1500
29.10.2026 4:31:31  1500

Between 4:31 and 7:20 the machine rebooted; after that the cycles come exactly 1 hour 46 minutes apart — 90 minutes plus an offset of 16 that doesn't change until the next reboot.


Sources

How a policy is built and the order of application:

Filters and permissions:

Administrative templates:

The GroupPolicy module and utilities:

Time and troubleshooting:


Policies have configured the machines. The next block is about how machines and people prove who they are not with a password but with a certificate, and about who issues those certificates.

Next — part 25: the AD CS certification authority. Why your own public key infrastructure, and how a two-tier hierarchy with an offline root is built; installing the root and issuing CAs with ADCSDeployment commands; certificate templates and their permissions; autoenrollment through Group Policy; revocation lists, their distribution points and OCSP; and what from part 23 of the networking series about TLS works here and what is arranged differently.

Only registered users can leave a comment.

Sign in or register to leave a comment.