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:
- 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.
- Finds a controller (event 5308 in the log) and asks it over LDAP for
gPLinkandgPOptionsof 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. - 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.
- 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. - Passes each extension on the list its files, and the extensions apply the settings: registry writes to
HKLM\SOFTWARE\PoliciesandHKCU\SOFTWARE\Policies, security settings are applied by theseceditengine, 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:
- Group Policy overview — what a GPO, GPC and GPT are, client-side extensions;
- Group Policy processing — LSDOU, enforced links and block inheritance, loopback, refresh intervals (90 minutes with an offset of up to 30, five minutes on controllers), what isn't applied in the background;
- Group Policy scope — the
gPLinkformat, link flags, the order of entries in the string, security and WMI filtering; - [MS-GPOL]: Group Policy: Core Protocol and Sample gpt.ini File — the specification: policy container and template,
versionNumberof two 16-bit halves,gPCMachineExtensionNames,flags,gPCWQLFilter,gpt.ini; - Loopback processing of Group Policy — Merge and Replace modes;
- What's New in Group Policy in Windows Server — policy caching, background refresh as a scheduled task,
Invoke-GPUpdate, infrastructure status; - Group Policy Preferences — preferences and item-level targeting;
- dcgpofix — restoring the two default policies and what to keep in them.
Filters and permissions:
- MS16-072: Security update for Group Policy and Deploying Group Policy Security Update MS16-072 — reading user policies with the computer account and Read permissions;
- ms-WMI-Som class — the class of a WMI filter object in the directory;
- Configuring Account Policies — account policies take effect from domain policies.
Administrative templates:
- Create and manage the Central Store — ADMX and ADML, the Central Store path, updating through a new folder.
The GroupPolicy module and utilities:
- GroupPolicy module — all cmdlets of the module;
- Get-GPInheritance, New-GPLink and Set-GPRegistryValue — inheritance, linking, registry values;
- Set-GPPermission — permission levels and
-Replace; - Invoke-GPUpdate — remote refresh through a scheduled task and the firewall rules it needs;
- Get-GPOReport and Backup-GPO — report and backup;
- gpupdate and gpresult — the switches of both utilities.
Time and troubleshooting:
- Windows Time service tools and settings — W32Time parameters and their values in Group Policy;
- Applying Group Policy troubleshooting guidance — the order of investigation, client logs and events.
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.