Servers Identity

Group Policy at Scale: A Maintainable Architecture and Managing GPOs as Code

Most Group Policy estates rot the same way. Years of click-ops in the Group Policy Management Console leave you with a few hundred GPOs nobody dares delete, a logon that takes ninety seconds because every workstation processes settings meant for three machines in a lab, and no answer to the simplest audit question: who changed the audit policy last quarter, and why? The Group Policy Management Console (GPMC) is a beautiful tool for editing one setting; it is a terrible system of record. It has no diff, no review gate, no history beyond a single-level “restore from backup”, and no way to stop a well-meaning engineer from linking a kitchen-sink GPO at the domain root at 4pm on a Friday.

The fix is not another tool bolted on top — it is a design you can read and a workflow that treats every GPO as a versioned artifact. When a GPO is code, a change is a pull request: it is reviewed by a second engineer, validated by CI before it touches a domain controller, diffed against the last known-good state, and reversible in one command when it breaks logon for the shop floor. This article is the architecture and the PowerShell pipeline I run for production Group Policy — the precedence model that decides which GPO wins, the targeting that keeps policy off the wrong machines, the GroupPolicy module’s backup/import/restore mechanics, LGPO.exe and the Microsoft Security Compliance Toolkit for baselines, representing GPOs in Git, gating changes with CI, the central ADMX store, RSoP-driven change control, cross-domain migration tables, and the advanced auditing that tells you the truth when the GPMC link list lies.

By the end you will be able to design a GPO estate a new hire can reason about, put its entire state under version control, block a bad change before it applies, and localise any “why isn’t my policy applying” ticket to the exact GPO, filter, or precedence rule that caused it — with real commands, expected output, and the gotchas that only show up at 40,000 seats.

Scope note: this is on-prem / IaaS Group Policy on Active Directory Domain Services (AD DS), managed from a domain-joined admin host with RSAT and the GroupPolicy PowerShell module. It is not Intune/MDM policy, and it is not Microsoft Entra Domain Services (which restricts GPO editing to two managed GPOs and blocks most of the tooling here). The principles transfer, but the cmdlets assume a forest you own. Where a setting overlaps with modern management, the Configuration Management for Windows Server with PowerShell DSC and Ansible article covers the desired-state side; Group Policy remains the native, domain-scoped mechanism and is not going away.

What problem this solves

Click-ops Group Policy fails in production in a small number of predictable, expensive ways. There is no review: a single admin edits a live GPO in GPMC and the change is instantly effective on the next gpupdate across every machine in scope — no second pair of eyes, no staging. There is no history: GPMC’s “back up” is a one-shot snapshot into a GUID-named folder, not a timeline; git blame-grade attribution simply does not exist. There is no validation: nothing stops you linking a GPO with a broken WMI filter, an empty settings body, or a security-filtering change that silently breaks the MS16-072 computer-Read requirement and stops the GPO applying for everyone. And there is no reproducibility: promoting a tested GPO from dev to prod is a manual re-creation in the destination console, which is how dev.contoso.com\GG-LabVMs ends up hard-coded, broken, in production.

What breaks without a code-based workflow is not usually a dramatic outage — it is slow rot and slow incidents. Logons degrade as untargeted preferences and heavy WMI filters accumulate. A “harmless” tweak to the security baseline disables a service half the fleet needed, and because there is no diff and no attribution, the root-cause investigation takes a day instead of a git log. An auditor asks for evidence of change control on the domain’s audit and password policy and you have screenshots. When a GPO does need rolling back, “restore from backup” reproduces settings but not links or WMI-filter associations, so the restored GPO sits there, correct but unlinked, doing nothing.

Who hits this: every team running Windows at scale — a 200-seat SMB with one over-broad Default Domain Policy, a 40,000-seat manufacturer with 280 kitchen-sink GPOs, a regulated shop that must prove who changed what. The common thread is that Group Policy is powerful, native, and unversioned by default, and the cost of leaving it unversioned compounds. Treating GPOs as code turns each of those failure modes into a solved problem: review becomes a PR, history becomes commits, validation becomes CI, and reproducibility becomes an Import-GPO with a migration table.

Before the deep dive, here is the whole problem space — each failure of click-ops Group Policy, what it costs, and the code-based practice that fixes it:

Click-ops failure What it costs in production The as-code practice that fixes it
No review before a live edit applies Bad change hits every machine on next refresh PR gate: GPO XML diff reviewed before GPMC is touched
No history / attribution Root-cause takes a day, not a git log Scheduled Backup-GPO + XML report committed to Git
No pre-apply validation Broken WMI filter / empty GPO / MS16-072 break ships CI lint: schema, filters, computer-Read, naming
Manual cross-domain re-creation Hard-coded stale principals/UNCs in prod Import-GPO with a reviewed migration table
One-shot “restore” loses links/filters Restored GPO is correct but unlinked, does nothing Script links + WMI filters separately; restore is idempotent
Baselines drift, unmeasured Security posture erodes silently SCT baselines + LGPO.exe + PolicyAnalyzer diff in CI
“Why isn’t it applying?” tickets Hours per ticket in the GPMC link list RSoP/gpresult as the ground truth, scripted

Learning objectives

By the end of this article you can:

Prerequisites & where this fits

You should already be comfortable with Active Directory fundamentals: what a domain, site, and organizational unit (OU) are, how objects live in OUs, and how AD replication moves changes between domain controllers (the Building an AD DS Forest the Right Way article is the upstream foundation, and Diagnosing AD Replication and FSMO Failures with repadmin and dcdiag covers what happens when the SYSVOL replication under Group Policy misbehaves). You should be able to run PowerShell as an administrator, install RSAT, and read XML. Basic Git literacy — add, commit, diff, branches, pull requests — is assumed; you do not need to be a Git expert, only to treat a repo as the system of record.

This sits at the intersection of identity/endpoint management and infrastructure-as-code. Group Policy is the native configuration channel for domain-joined Windows; putting it under version control is the same instinct that puts your Terraform and Bicep under version control. It pairs with the tiered-admin model (GPOs are how you enforce Tier-0 isolation and the security baseline), with Hardening Windows Server and Building a Reliable WSUS Patch Pipeline (Windows Update settings are delivered by GPO), and with credential hardening via Eliminating Static Service Credentials with gMSA and Windows LAPS (LAPS and its policy are configured by GPO). Where this fits in the bigger picture is simple: it is the layer that makes every other Windows control durable, reviewable, and reversible.

Here is where each moving part of Group Policy physically lives, so the “as-code” boundaries are clear from the start:

Component Physical location Replicates via In a backup? Managed as code by
GPO container (GPC) AD: CN=Policies,CN=System,<domain DN> AD replication Settings yes, object metadata partial Backup-GPO / Import-GPO
GPO template (GPT) SYSVOL: \\<domain>\SYSVOL\<domain>\Policies\{GUID} DFS-R (SYSVOL) Yes (files) Same
GPO links OU/site/domain gPLink attribute AD replication No Custom script (New-GPLink)
WMI filters AD: CN=SOM,CN=WMIPolicy,... AD replication No Custom export/import
Security filtering (ACL) On the GPO object (AD ACL) AD replication Partial (in report) Set-GPPermission script
ADMX/ADML templates Central Store in SYSVOL, or local PolicyDefinitions DFS-R / local No Git-managed Central Store
Local GPO %windir%\System32\GroupPolicy on each host Not replicated N/A LGPO.exe

Core concepts

Six mental models make every later section obvious. Internalise these and the “why isn’t it applying” tickets mostly answer themselves.

A GPO is two linked objects, not a file. Every GPO is a Group Policy Container (GPC) in AD (holding version numbers, the WMI-filter link, and the security ACL) plus a Group Policy Template (GPT) in SYSVOL (holding the actual registry.pol, security template, scripts, and ADMX-driven settings), joined by a GUID. When you “edit a GPO”, GPMC writes to both and bumps a version number on each; if the AD version and the SYSVOL version disagree after replication, clients can apply stale or partial settings. This is why “managing GPOs as code” means capturing both halves — the module’s Backup-GPO does exactly that, but links and WMI filters live outside the GPO and must be handled separately.

Precedence is deterministic, and last writer wins. Group Policy applies in LSDOU order (Local → Site → Domain → OU, parent OU down to the object’s OU). For any single conflicting setting, the GPO processed last wins. Within one container, multiple links apply by link order, where the lowest order number has the highest precedence (it is processed last). So a deep OU beats a shallow OU beats the domain beats the site beats local — unless a modifier bends it.

Two modifiers bend precedence, and they cause most confusion. Block Inheritance on an OU stops GPOs linked above it from flowing down. Enforced (formerly “No Override”) on a link makes that GPO immune to Block Inheritance and flips it to win over lower links. Both are legitimate but both hide the true precedence from the next engineer, so both should be rare, documented exceptions — not tools of first resort.

Linking decides where; filtering decides which objects apply it. A GPO linked to an OU is merely considered for every object in that OU. Whether a given object actually applies it depends on security filtering (the object needs both Read and Apply group policy), WMI filtering (a client-evaluated query must return results), and — for Group Policy Preferences — item-level targeting per individual item. Filtering is where you get precision without exploding your OU count.

Loopback inverts the user/computer model for shared machines. Normally user settings follow the user object regardless of which machine they log on to. Loopback processing (configured in a computer GPO on the machine’s OU) makes user policy driven by the machine — essential for kiosks, RDS/Citrix hosts, and labs, and the single most misunderstood feature in Group Policy.

“As code” means the repo is the system of record, not GPMC. The discipline is: the live domain is a deployment target, the Git repo is the source of truth, and changes flow repo → review → CI → apply. GPMC becomes the thing CI drives and humans read, not the place humans make unreviewed live edits. Everything below serves that inversion.

The vocabulary in one table

Pin down every term before the deep sections; the glossary repeats these for lookup, but this is the mental model side by side:

Term One-line definition Where it lives Why it matters to “as code”
GPC Group Policy Container (AD half) AD CN=Policies Holds version, WMI link, ACL
GPT Group Policy Template (SYSVOL half) SYSVOL Policies\{GUID} Holds the actual settings files
LSDOU Processing order Local→Site→Domain→OU Client apply logic Determines the winning setting
Link order Precedence within one container gPLink on the container Lowest number wins
Block Inheritance Stops inherited GPOs at an OU OU attribute Hides precedence; use sparingly
Enforced Link that overrides Block/lower links On the link Keeps baselines mandatory
Security filtering ACL: Read + Apply required GPO object ACL Cheapest targeting; MS16-072 gotcha
WMI filter Client-evaluated query gate AD WMIPolicy Flexible but costs CPU per refresh
Item-level targeting (ILT) Per-preference-item condition Inside a Preferences GPO Finest targeting available
Loopback User policy driven by the computer Computer GPO, machine OU Kiosk/RDS answer; often misapplied
Central Store Shared ADMX/ADML in SYSVOL SYSVOL PolicyDefinitions One template set for all editors
RSoP Resultant Set of Policy gpresult / RSoP report Ground truth for what applied
Migration table Source→destination reference map .migtable file Cross-domain promotion
LGPO.exe Local GPO import/export tool Security Compliance Toolkit Manage local policy as code
SCT Security Compliance Toolkit Microsoft download Baselines + PolicyAnalyzer

Precedence, inheritance, blocking, and enforcement

Everything downstream depends on knowing which GPO wins. Get this wrong and no amount of tooling saves you — you will version-control a policy that never applies.

LSDOU and link order

Group Policy applies in LSDOU order — Local, then Site, then Domain, then OU from the topmost parent down to the OU that directly contains the object. Later in the order overrides earlier, setting by setting. Within a single container that has several linked GPOs, precedence is by link order: the GPO with link order 1 is processed last and therefore wins. This inversion (lowest number = highest precedence) trips up everyone once.

Inspect inheritance and link order for an OU in one shot:

Import-Module GroupPolicy
# GpoInheritanceBlocked tells you if the OU blocks; the GpoLinks list is in precedence order.
Get-GPInheritance -Target "OU=Workstations,DC=corp,DC=contoso,DC=com" |
    Select-Object -ExpandProperty GpoLinks |
    Format-Table DisplayName, Enabled, Enforced, Order -AutoSize

The full precedence model, from lowest to highest, and what each layer is for:

Precedence layer Applied order Wins over Typical use Gotcha
Local GPO 1st (lowest) Nothing Non-domain / DC-of-last-resort Overwritten by any domain GPO with the same setting
Site-linked 2nd Local Proxy/site-specific network settings Rare; easy to forget it exists
Domain-linked 3rd Local, Site Password/account, top baselines Do not turn Default Domain Policy into a dumping ground
OU-linked (shallow→deep) 4th (highest) All above Role/exception targeting Deeper OU beats shallower OU
Enforced link Overrides all Even deeper OUs Mandatory security baseline Flips normal precedence; document every one
Block Inheritance N/A (blocks) Blocks inherited (not Enforced) Isolate a sensitive OU Invisible in the link list; confuses the next admin

Block Inheritance and Enforced

Block Inheritance is an OU property; it stops GPOs linked above the OU from applying to objects in it. Enforced is a link property; an enforced GPO cannot be blocked by Block Inheritance and wins over lower-precedence links. The interaction is the part people get wrong: Enforced beats Block Inheritance every time. So a security baseline linked-and-enforced at the domain root reaches even into a Block-Inheritance OU — which is exactly why you enforce baselines and why an OU can never “escape” a mandatory control by blocking.

Audit every use of both across the estate — this is the first thing I script in a new environment because sprawl here is where precedence goes to die:

# Enumerate every enforced link and every Block-Inheritance OU in the domain.
$root = (Get-ADDomain).DistinguishedName
Get-ADOrganizationalUnit -Filter * -SearchBase $root | ForEach-Object {
    $inh = Get-GPInheritance -Target $_.DistinguishedName
    if ($inh.GpoInheritanceBlocked -eq 'Yes') {
        [pscustomobject]@{ Type='BlockInheritance'; Target=$_.DistinguishedName; Detail='' }
    }
    $inh.GpoLinks | Where-Object Enforced -eq 'Yes' | ForEach-Object {
        [pscustomobject]@{ Type='Enforced'; Target=$_.Target; Detail=$_.DisplayName }
    }
} | Format-Table -AutoSize

The decision table for when each modifier is legitimate versus a smell:

If you’re reaching for… It’s legitimate when… It’s a smell when… Better alternative
Block Inheritance An OU must be isolated from all inherited policy (e.g. a jump-host OU) You use it to dodge one unwanted GPO Re-scope or re-link that one GPO
Enforced Making a security baseline mandatory top-down You enforce role GPOs “to be safe” Fix link order / OU depth instead
Both together A hardened Tier-0 OU that blocks all but enforced baselines Routine, undocumented, everywhere Redesign the OU tree

The rule I hold teams to: every Enforced link and every Block Inheritance is a documented exception with an owner and a review date. Each one is a small lie about precedence the next engineer has to discover. If you reach for them routinely, your OU design is wrong — model OUs around what you delegate and target, not the org chart.

A layered design: baseline, role, and exception GPOs

The single highest-leverage decision is to stop making “kitchen-sink” GPOs. Adopt a layered model where each GPO has one job and a name that says so.

The naming convention

Encode scope, layer, target, and purpose directly in the GPO name:

<Scope>-<Layer>-<Target>-<Purpose>

C-BASE-AllComputers-SecurityBaseline      # baseline, computer
U-BASE-AllUsers-SecurityBaseline          # baseline, user
C-ROLE-Servers-RDPHardening               # role, computer
C-ROLE-Kiosk-ShellLockdown                # role, computer
U-ROLE-Finance-DriveMappings              # role, user (Preferences)
C-EXC-LabVMs-AllowLegacyTLS               # exception, computer, temporary

C-/U- says which half is active; BASE/ROLE/EXC says the layer; the target and purpose predict the contents. A GPO estate where the name and link tell the whole story is one a new hire can reason about; a pile of GUID-named Default-ish GPOs is not.

The three layers, side by side:

Layer Prefix Linked where Scope Enforced? Lifetime Example
Baseline BASE Domain or top-level Computers/Users OU Broad (all objects) Usually yes Permanent C-BASE-AllComputers-SecurityBaseline
Role ROLE Role OU (Servers, Kiosk, Finance) The role only No Permanent C-ROLE-Servers-RDPHardening
Exception EXC Narrow OU, security-filtered Tightly scoped No Temporary C-EXC-LabVMs-AllowLegacyTLS

Split computer and user, disable the unused half

Splitting computer and user settings (the C-/U- prefix) lets you disable the unused half via GPO Status. A GPO with its user half disabled skips the user client-side extension entirely at apply time — free performance and a habit worth enforcing.

# Disable the user half of a computer-only GPO so the user extension is skipped at apply time.
(Get-GPO -Name "C-BASE-AllComputers-SecurityBaseline").GpoStatus = "UserSettingsDisabled"

The four GPO status values and what each does at apply time:

GpoStatus value Computer settings User settings When to use
AllSettingsEnabled Applied Applied GPO genuinely needs both halves
UserSettingsDisabled Applied Skipped Computer-only GPO (C- prefix)
ComputerSettingsDisabled Skipped Applied User-only GPO (U- prefix)
AllSettingsDisabled Skipped Skipped GPO staged but not yet live

Design the OU tree for delegation and targeting, not the org chart. A durable shape places a hardened OU=Tier0 for DC-adjacent admin hosts and PAWs (an enforced hardening baseline, deliberately blocked from lower policy); OU=Servers/<role> for member servers carrying C-ROLE-<role>-* hardening; OU=Workstations/<type> for laptops, desktops, and kiosks carrying C-BASE plus a device-type C-ROLE; OU=Users/<function> for accounts carrying U-BASE plus a function U-ROLE; and a restrictive OU=ServiceAccounts for gMSA/service identities (least privilege, often deny-logon). The rule underneath every one of those: an OU exists because you delegate it or target it, never because the org chart has a box.

Targeting: security filtering, WMI filters, and item-level targeting

Linking decides where a GPO is considered. Filtering decides which objects inside that scope apply it. Three mechanisms, in rough order of preference.

Security filtering (cheapest — prefer this)

A GPO applies to a principal only if that principal has both Read and Apply group policy on the GPO. The default scope is Authenticated Users. To target a GPO, grant Apply to a specific group and remove the broad Apply — but keep Read for computers.

# Scope an exception GPO to one AD group instead of Authenticated Users.
$gpo = "C-EXC-LabVMs-AllowLegacyTLS"
# Grant Apply (Read + Apply group policy) to the target group...
Set-GPPermission -Name $gpo -TargetName "GG-LabVMs" -TargetType Group -PermissionLevel GpoApply
# ...remove Apply from Authenticated Users, but re-grant Read so computers can still read it (MS16-072).
Set-GPPermission -Name $gpo -TargetName "Authenticated Users" -TargetType Group -PermissionLevel None
Set-GPPermission -Name $gpo -TargetName "Domain Computers"   -TargetType Group -PermissionLevel GpoRead

The MS16-072 permission levels and what each maps to:

PermissionLevel (cmdlet) Read Apply group policy Effect Use for
GpoRead Yes No Can read, does not apply Computers on a user-filtered GPO (MS16-072)
GpoApply Yes Yes Reads and applies The actual target group
GpoEdit Yes No (+ write) Delegated editing GPO administrators
GpoEditDeleteModifySecurity Yes No (+ full) Full control Owners only
None No No Removed Strip the default broad Apply

The MS16-072 trap. Since MS16-072 (the June 2016 hardening), GPOs are retrieved in the computer’s security context, so a GPO filtered to only a user group silently stops applying unless the computer account (or Domain Computers / Authenticated Users) also has Read. The pattern: filter Apply to your user group, but leave Authenticated Users or Domain Computers with Read-only. Removing Read entirely from computers is the classic post-patch outage — the GPO looks correct in GPMC and applies to nobody.

Audit the whole estate for GPOs that would break under MS16-072 — no computer-readable principal has Read:

# Flag GPOs where neither Authenticated Users nor Domain Computers can Read (MS16-072 risk).
Get-GPO -All | ForEach-Object {
    $perms = Get-GPPermission -Guid $_.Id -All
    $computerRead = $perms | Where-Object {
        $_.Trustee.Name -in 'Authenticated Users','Domain Computers','Domain Controllers' -and
        $_.Permission -in 'GpoRead','GpoApply'
    }
    if (-not $computerRead) {
        [pscustomobject]@{ GPO=$_.DisplayName; Risk='No computer Read (MS16-072)' }
    }
} | Format-Table -AutoSize

WMI filters (flexible, but they cost CPU)

A WMI filter is a query evaluated on the client at apply time; the GPO applies only if the query returns results. Useful for “laptops only” or “member servers only” without separate OUs.

-- Member servers only (ProductType: 1=workstation, 2=domain controller, 3=member server)
SELECT * FROM Win32_OperatingSystem WHERE ProductType = "3"
-- Laptop / notebook chassis types only
SELECT * FROM Win32_SystemEnclosure WHERE ChassisTypes = 9 OR ChassisTypes = 10 OR ChassisTypes = 14

Every WMI filter runs on every machine in scope at every refresh. One badly-written filter taxes every logon. The cost hierarchy by WMI class:

WMI class used Relative cost Safe? Notes
Win32_OperatingSystem Very low Yes Preferred; version/ProductType checks
Win32_ComputerSystem Low Yes Domain role, manufacturer
Win32_SystemEnclosure Low Yes Chassis type (laptop detection)
Win32_NetworkAdapterConfiguration Medium Careful Enumerates adapters; can be slow
Win32_Product Very high No Triggers MSI self-repair on every enumeration
root\CIMV2 joins across classes High No Compounds per-machine cost

Never use Win32_Product. Enumerating it triggers an MSI self-consistency check (a msiexec reconciliation) for every installed product on every refresh. On a large fleet this alone can add tens of seconds to every logon. Replace “is app X installed” checks with registry-presence item-level targeting, which costs nothing.

Item-level targeting (for Group Policy Preferences)

Group Policy Preferences (drive maps, printers, registry, scheduled tasks, files) support item-level targeting (ILT) on each individual item — far finer than per-GPO filtering. One Preferences GPO can map the Finance drive only when the user is in a group, on a given subnet, and on a laptop. ILT is evaluated per item, so it is the right tool when one GPO must behave differently for different objects rather than splitting into many GPOs.

The targeting-mechanism decision matrix:

Mechanism Granularity Evaluated CPU cost Best for Avoid when
Link scope (OU) Per container AD lookup ~none Structural, permanent targeting You’d create an OU per exception
Security filtering Per principal ACL check Very low Targeting a group within an OU You need per-item logic
WMI filter Per machine Client query Low–high “laptops/servers only” without new OUs A group or OU split would do
Item-level targeting Per preference item Client, per item Low One Preferences GPO, many conditions Non-Preferences settings (ILT only works on Preferences)

Loopback processing for shared and kiosk machines

The default model applies user settings based on where the user object lives, regardless of which machine they log on to. That breaks for kiosks, RDS/Citrix session hosts, and lab machines, where you want user policy driven by the machine. Loopback processing does that — enabled in a computer GPO linked to the machine’s OU.

Computer Configuration
  -> Administrative Templates
    -> System -> Group Policy
      -> Configure user Group Policy loopback processing mode = Enabled (Replace)

Set it as code (it is a registry policy under the computer half):

# Enable loopback Replace on the kiosk computer GPO. 1 = Replace, 2 = Merge.
Set-GPRegistryValue -Name "C-ROLE-Kiosk-ShellLockdown" `
    -Key "HKLM\Software\Policies\Microsoft\Windows\System" `
    -ValueName "UserPolicyMode" -Type DWord -Value 1

The two modes, precisely:

Mode UserPolicyMode What applies User’s own user GPOs Use for
Merge 2 User’s normal GPOs then the computer-OU user GPOs (latter wins on conflict) Applied first Corp desktops needing a few machine overrides
Replace 1 Only the user GPOs linked to the computer’s OU Ignored entirely Kiosks/RDS — everyone gets the identical locked shell

Loopback is the single most misunderstood feature in Group Policy. Two rules keep you sane. First, it lives in the computer half of a GPO linked to the computer’s OU — not the user’s. Second, the user settings it pulls in must be reachable from that computer’s OU path (linked somewhere on the computer’s OU chain). If your kiosk lockdown “isn’t applying,” 90% of the time the user-config GPO simply isn’t linked anywhere the computer’s OU can see.

Four loopback failures cover almost every ticket: the lockdown not applying (the user GPO isn’t linked on the computer’s OU chain — confirm with gpresult /scope user /r on the kiosk, fix by linking the U- GPO there); admins getting locked down too (Replace mode with no exclusion — security-filter the lockdown to exclude the admin group); user settings leaking through (Merge when you meant Replace — set UserPolicyMode to 1); and loopback being ignored entirely (someone set it in the user half by mistake — move it to the computer half). These four also appear in the master troubleshooting playbook later, so you never have to remember them out of context.

Backup, report, and diff GPOs with the GroupPolicy module

Now treat GPOs as artifacts. The GroupPolicy module gives you backup, restore, import, and HTML/XML reporting — enough to build a real pipeline. Here is the cmdlet surface you will script:

Cmdlet Purpose Captures Notes
Backup-GPO Back up one/all GPOs to a folder Settings (GPC+GPT), report Not links or WMI filters
Restore-GPO Restore into the same domain by GUID/name Settings Recreates the GPO if deleted
Import-GPO Import settings into a GPO (any domain) Settings, with migration table For promotion/migration
Get-GPOReport HTML/XML report of settings All settings, diff-friendly XML The Git-friendly artifact
New-GPLink / Set-GPLink Create/modify a link Links are separate from backup
Set-GPPermission Security filtering ACL MS16-072 handling
Get-GPInheritance Inheritance/link order for a target Links, block state Read model
Get-GPResultantSetOfPolicy RSoP report for user+computer Effective result Verification

Full-estate backup

# Timestamped estate backup with a manifest that Import/Restore can read.
$stamp = Get-Date -Format "yyyy-MM-dd_HHmmss"
$root  = "C:\GPOBackups\$stamp"
New-Item -ItemType Directory -Path $root | Out-Null
Backup-GPO -All -Path $root -Comment "Scheduled backup $stamp"

Backup-GPO -All writes each GPO into a GUID-named subfolder containing Backup.xml, bkupInfo.xml, the registry/settings data, and the GPO’s report — plus a manifest.xml at the root mapping friendly names to backup IDs, which is what Import-GPO/Restore-GPO read.

Diff-friendly XML reports

GUID folders are useless to a human and noisy in Git. Export each GPO to an XML report keyed by display name — XML diffs cleanly and captures the actual settings, not opaque blobs:

# One stable, diff-friendly XML report per GPO, named by display name.
$reportDir = "C:\GPOReports"
New-Item -ItemType Directory -Path $reportDir -Force | Out-Null
Get-GPO -All | ForEach-Object {
    $safe = $_.DisplayName -replace '[\\/:*?"<>|]', '_'
    Get-GPOReport -Guid $_.Id -ReportType Xml -Path (Join-Path $reportDir "$safe.xml")
}

Compare two snapshots of the same GPO — before and after a change:

$before = [xml](Get-Content "C:\GPOReports-old\C-BASE-AllComputers-SecurityBaseline.xml")
$after  = [xml](Get-Content "C:\GPOReports\C-BASE-AllComputers-SecurityBaseline.xml")
Compare-Object ($before.OuterXml -split "`n") ($after.OuterXml -split "`n")

Use the two report formats for different jobs: Html is rich and human-friendly for change reviews and audit evidence but diffs poorly (layout noise swamps the real delta), while Xml is the one you commit — it diffs cleanly and is programmatically comparable, which is exactly what CI needs.

What Backup-GPO does not capture — the number-one restore surprise. It backs up settings only. It does not back up links (they live on the OU/site/domain gPLink), WMI-filter definitions, or the filter-to-GPO association. A “restore” reproduces the settings into an unlinked, unfiltered GPO. You must script links and WMI filters separately (below) or your restore silently does nothing.

The complete “what’s in a backup, what isn’t” reference:

Artifact In Backup-GPO? How to capture separately Restore step
Registry/policy settings Yes Restore-GPO / Import-GPO
Security template / audit policy Yes Same
Scripts (logon/startup) Yes (in GPT) Same
GPO ACL (security filtering) In the report, not re-applied Script Get-GPPermission Replay Set-GPPermission
Links (gPLink) No Export via Get-GPInheritance per OU Replay New-GPLink
WMI filter definitions No Export msWMI-Som objects Recreate + relink
WMI filter → GPO association No Record gPCWQLFilter Re-associate

Exporting the pieces a backup misses: links and WMI filters

Because links and WMI filters live outside the GPO, capture them as their own code artifacts.

Export and replay links

# Export every GPO link in the domain to a CSV you can commit and replay.
$root = (Get-ADDomain).DistinguishedName
$targets = @($root) +
    (Get-ADOrganizationalUnit -Filter * -SearchBase $root).DistinguishedName
$links = foreach ($t in $targets) {
    (Get-GPInheritance -Target $t).GpoLinks | ForEach-Object {
        [pscustomobject]@{
            Target   = $t
            GpoName  = $_.DisplayName
            Enabled  = $_.Enabled
            Enforced = $_.Enforced
            Order    = $_.Order
        }
    }
}
$links | Export-Csv "C:\git\gpo-repo\links.csv" -NoTypeInformation

Replay them (idempotently) after a restore or in a new domain:

Import-Csv "C:\git\gpo-repo\links.csv" | ForEach-Object {
    $existing = (Get-GPInheritance -Target $_.Target).GpoLinks |
        Where-Object DisplayName -eq $_.GpoName
    if (-not $existing) {
        New-GPLink -Name $_.GpoName -Target $_.Target `
            -LinkEnabled $_.Enabled -Enforced $_.Enforced -Order ([int]$_.Order)
    } else {
        Set-GPLink -Name $_.GpoName -Target $_.Target `
            -LinkEnabled $_.Enabled -Enforced $_.Enforced -Order ([int]$_.Order)
    }
}

Export WMI filters

WMI filters are msWMI-Som objects in AD; export their definitions so they can be recreated:

# Export all WMI filter definitions from AD to committable objects.
$wmiPath = "CN=SOM,CN=WMIPolicy,CN=System,$((Get-ADDomain).DistinguishedName)"
Get-ADObject -Filter 'objectClass -eq "msWMI-Som"' -SearchBase $wmiPath `
    -Properties 'msWMI-Name','msWMI-Parm1','msWMI-Parm2' |
    Select-Object @{n='Name';e={$_.'msWMI-Name'}},
                  @{n='Description';e={$_.'msWMI-Parm1'}},
                  @{n='Query';e={$_.'msWMI-Parm2'}} |
    Export-Csv "C:\git\gpo-repo\wmi-filters.csv" -NoTypeInformation

Together these give you a backup model that actually round-trips a GPO estate: Backup-GPO -All under backups/ restores the settings, links.csv restores the links, wmi-filters.csv restores the filters, and a permissions/<name>.csv (from Get-GPPermission) restores the security filtering — with the reports/<name>.xml alongside as the human-readable diff. Miss any one of those files and the restore is incomplete in exactly the silent way described above.

Local policy and baselines as code: LGPO.exe and the Security Compliance Toolkit

Domain GPOs cover domain-joined machines. Local GPO covers the rest — standalone hosts, DMZ boxes, images before domain-join, and the “belt and braces” hardening you want even if a domain GPO fails. Microsoft’s Security Compliance Toolkit (SCT) ships LGPO.exe (import/export local policy as text) and PolicyAnalyzer (diff policy sets), alongside downloadable security baselines for each Windows/Server version.

LGPO.exe — local policy as text

LGPO.exe converts local policy to and from a plain-text .PolicyRules / LGPO-text format you can commit, review, and diff.

:: Export the current local GPO to a text file you can commit to Git.
LGPO.exe /parse /m C:\Windows\System32\GroupPolicy\Machine\registry.pol > machine-policy.txt

:: Apply a reviewed baseline text file to the local machine.
LGPO.exe /t baseline-machine.txt

:: Import a full baseline backup (GPO backup folder) to the local policy.
LGPO.exe /g C:\Baselines\Windows11-v24H2\GPOs

The LGPO.exe mode reference:

Switch Action Input Output Use in pipeline
/parse /m Parse a registry.pol to text .pol LGPO text Export current state for diff
/parse /s Parse a .inf security template .inf text Export security settings
/t Apply LGPO text text Local GPO Apply reviewed policy
/g Apply a GPO backup folder backup dir Local GPO Apply SCT baseline
/r Build registry.pol from text text .pol Compose policy in CI
/b Back up local GPO to a folder backup dir Snapshot before change

Security Compliance Toolkit — baselines and PolicyAnalyzer

Download the SCT and the baseline for your OS. Each baseline is a folder of GPO backups (Domain, plus MS-only / DC variants), ADMX, and a PolicyAnalyzer rules file. PolicyAnalyzer loads multiple policy sets and produces a matrix of every difference — baseline vs your live policy, or one baseline version vs the next.

# Import the Windows security baseline into the domain as real GPOs (from the SCT download).
# Each baseline ships a Backup-GPO folder; import each into a named GPO, then link + filter.
$baseline = "C:\SCT\Windows11-24H2-Security-Baseline\GPOs"
Get-ChildItem $baseline -Directory | ForEach-Object {
    $manifest = Join-Path $_.FullName "..\manifest.xml"
}
Import-GPO -BackupGpoName "MSFT Windows 11 24H2 - Computer" `
    -TargetName "C-BASE-AllComputers-MSFTBaseline" `
    -Path "C:\SCT\Windows11-24H2-Security-Baseline" -CreateIfNeeded

Diff live policy against a baseline in CI so drift becomes a build signal (PolicyAnalyzer exposes a CSV export you can compare):

SCT component What it does Input Output CI use
Baselines Microsoft-recommended settings per OS GPO backup folders Import as MSFTBaseline GPOs
PolicyAnalyzer Diff N policy sets into a matrix GPO backups / effective policy Difference grid + CSV Baseline-vs-live drift check
LGPO.exe Import/export local policy as text .pol/.inf/text text/.pol/local GPO Apply + snapshot local policy
GPO2PolicyRules Convert a GPO backup to PolicyRules GPO backup .PolicyRules Feed PolicyAnalyzer from a domain GPO

The decision on where a control belongs — domain GPO, local GPO, or both:

Control Domain GPO Local GPO (LGPO) Rationale
Password/account policy Yes (domain root) No Domain-wide by definition
Security baseline (member server) Yes (C-BASE) Yes (image build) Belt and braces before domain-join
DMZ / non-domain host hardening N/A Yes No domain to deliver it
Golden-image pre-hardening No Yes Applied before the machine ever joins
Emergency stopgap on one host No Yes (temporary) Faster than a GPO, but track it

Version control: representing GPOs in Git

Point the export at a Git working tree and you have GitOps for Group Policy — every change a reviewable commit, every state recoverable, and git blame answering “who changed the audit policy and when.”

The repository layout

gpo-repo/
  backups/              # Backup-GPO -All output (per-GUID, with manifest.xml)
  reports/              # Get-GPOReport -Xml, named by display name (the human diff)
  permissions/          # Get-GPPermission per GPO (security filtering)
  links.csv             # every gPLink, exported
  wmi-filters.csv       # msWMI-Som definitions
  admx/                 # Central Store ADMX/ADML, versioned
  baselines/            # SCT baselines + LGPO text
  scripts/              # Export-GpoEstate.ps1, Import-GpoEstate.ps1, Validate-Gpo.ps1
  .gitignore
  README.md

The scheduled snapshot

Run on a schedule from a hardened admin host so drift is captured even when someone edits live:

# Export-GpoEstate.ps1 — refresh the repo from live, commit any drift.
Set-Location C:\git\gpo-repo
Backup-GPO -All -Path .\backups | Out-Null
Get-GPO -All | ForEach-Object {
    $safe = $_.DisplayName -replace '[\\/:*?"<>|]', '_'
    Get-GPOReport -Guid $_.Id -ReportType Xml -Path ".\reports\$safe.xml"
    Get-GPPermission -Guid $_.Id -All |
        Select-Object @{n='Trustee';e={$_.Trustee.Name}}, Permission, Inherited |
        Export-Csv ".\permissions\$safe.csv" -NoTypeInformation
}
& .\scripts\Export-Links.ps1
& .\scripts\Export-WmiFilters.ps1

git add -A
git diff --cached --quiet
if ($LASTEXITCODE -ne 0) {
    git commit -m "GPO snapshot $(Get-Date -Format s) on $env:COMPUTERNAME"
    git push
}

The two operating models — and why you usually run both:

Model Source of truth Flow Pros Cons
Snapshot / audit Live domain Live → export → commit Zero disruption; captures ad-hoc edits Doesn’t prevent bad changes
GitOps / apply Git repo PR → CI → apply → domain True review gate; reproducible Requires discipline; no live edits
Hybrid (recommended) Repo for changes, snapshot for drift Both Review gate + drift detection Two pipelines to maintain

Hygiene is simple: commit the artifacts that are the record — reports/*.xml (the reviewable human diff), backups/** (the restorable, versioned artifact), and links.csv/wmi-filters.csv (the pieces backups miss). Ignore noise (*.log, temp exports). And never commit anything with a secret in a script — if a credential ever landed in Git history, it is compromised forever and must be rotated, because history does not forget.

CI validation: gate changes before they touch a domain controller

The payoff of GitOps is a CI pipeline that lints and diffs a GPO change before it ever applies. Run it on every pull request against the repo. The validations that catch the failures this whole article is about:

Check Catches How Severity
XML well-formedness Corrupt/partial report [xml] parse each report Error
Naming convention Rogue/unclear GPO names Regex ^[CU]-(BASE|ROLE|EXC)- Error
Empty GPO body A linked GPO with no settings Report has no <Extension> data Warn
MS16-072 computer Read Silent no-apply after filtering No Domain Computers/Auth Users Read Error
Win32_Product in a WMI filter Logon-tax filter Grep wmi-filters.csv for the class Error
Enforced/Block sprawl Undocumented precedence lies Count vs an allow-list Warn
Unlinked GPO Dead GPO accumulating In reports/ but not links.csv Warn
Diff summary Reviewer can’t see the change Post GPO XML diff as a PR comment Info

A Validate-Gpo.ps1 you can drop into a pipeline step:

# Validate-Gpo.ps1 — fails the build on policy anti-patterns. Run in CI on every PR.
param([string]$RepoRoot = ".")
$errors = @()

# 1. Naming convention on every report file.
Get-ChildItem "$RepoRoot\reports\*.xml" | ForEach-Object {
    if ($_.BaseName -notmatch '^[CU]-(BASE|ROLE|EXC)-') {
        $errors += "Naming: '$($_.BaseName)' violates <Scope>-<Layer>-<Target>-<Purpose>"
    }
    try { [xml](Get-Content $_.FullName) | Out-Null }
    catch { $errors += "XML: '$($_.BaseName)' is not well-formed" }
}

# 2. No Win32_Product in any WMI filter.
if (Test-Path "$RepoRoot\wmi-filters.csv") {
    Import-Csv "$RepoRoot\wmi-filters.csv" |
        Where-Object Query -match 'Win32_Product' |
        ForEach-Object { $errors += "WMI: filter '$($_.Name)' uses Win32_Product (logon tax)" }
}

# 3. MS16-072 — every GPO must have a computer-readable Read.
Get-ChildItem "$RepoRoot\permissions\*.csv" | ForEach-Object {
    $perms = Import-Csv $_.FullName
    $ok = $perms | Where-Object {
        $_.Trustee -in 'Authenticated Users','Domain Computers','Domain Controllers' -and
        $_.Permission -in 'GpoRead','GpoApply','GpoRead, GpoApply'
    }
    if (-not $ok) { $errors += "MS16-072: '$($_.BaseName)' has no computer Read" }
}

if ($errors) {
    $errors | ForEach-Object { Write-Error $_ }
    exit 1
}
Write-Host "GPO validation passed." -ForegroundColor Green

Produce a reviewer-friendly diff comment on each PR so a human sees exactly what changed:

# Post the XML diff of changed GPO reports to the PR (pseudo — wire to your CI's comment API).
git diff --unified=3 origin/main -- reports/ |
    Out-File $env:GPO_DIFF_FILE -Encoding utf8
# Your pipeline step then posts $GPO_DIFF_FILE as a PR comment.

The pipeline stages, end to end:

Stage Runs On Blocks merge?
Lint Validate-Gpo.ps1 PR open/update Yes (on Error)
Diff XML diff → PR comment PR open/update No
Human review 2nd engineer approves PR Yes (branch policy)
Apply Import-GpoEstate.ps1 to a staging domain Merge to main
Verify RSoP against a test object Post-apply Rollback on fail
Promote Migration-table import to prod Manual approval

The central ADMX store

Administrative Templates are defined by ADMX (settings) and ADML (language) files. By default each admin’s machine uses its local C:\Windows\PolicyDefinitions, so two admins on different Windows versions see different available settings and can create GPOs referencing templates the other cannot read. The Central Store fixes this: a single PolicyDefinitions folder in SYSVOL that every GPMC editor uses automatically.

# Create the Central Store and seed it from a current, patched admin host.
$domain = (Get-ADDomain).DNSRoot
$central = "\\$domain\SYSVOL\$domain\Policies\PolicyDefinitions"
New-Item -ItemType Directory -Path $central -Force | Out-Null
Copy-Item "C:\Windows\PolicyDefinitions\*.admx" $central -Force
New-Item -ItemType Directory -Path "$central\en-US" -Force | Out-Null
Copy-Item "C:\Windows\PolicyDefinitions\en-US\*.adml" "$central\en-US" -Force

Local vs Central Store, and why Central wins for an as-code shop:

Aspect Local PolicyDefinitions Central Store (SYSVOL)
Location Per admin machine \\<domain>\SYSVOL\...\PolicyDefinitions
Consistency Varies by each admin’s OS/patch One set for every editor
Precedence in GPMC Used only if no Central Store Overrides local when present
Version drift risk High Low (managed in one place)
As-code fit Poor Good — version the ADMX in Git

Five operational rules keep the Central Store sane. Seed it from the newest patched OS you manage (newer ADMX is a superset, so you never lose older settings). Version the ADMX/ADML in Git — the template set is part of your policy code. Add product ADMX (Office, Edge, Chrome) deliberately, importing only the versions you actually deploy. Update the store before editing new settings, or you create unreadable references against stale ADMX. And never hand-edit ADMX in SYSVOL — always replace from the Git-managed copy.

Change control and RSoP testing

Group-policy change control is not paperwork — it is predicting and verifying the effective result. The tools are gpresult (client-side, real logon) and Get-GPResultantSetOfPolicy (RSoP report, planning or logging mode). Never trust a link list; confirm the effective result on a real object.

The change-control workflow

# 1. BEFORE: capture the current effective state for the test target.
Get-GPResultantSetOfPolicy -ReportType Html -Path C:\temp\rsop-before.html `
    -User CONTOSO\jdoe -Computer WKS-014

# 2. Apply the change in a STAGING scope (or a slot GPO linked to a test OU) via the pipeline.

# 3. Force a refresh on the test client and re-capture.
Invoke-GPUpdate -Computer WKS-014 -Force
Get-GPResultantSetOfPolicy -ReportType Html -Path C:\temp\rsop-after.html `
    -User CONTOSO\jdoe -Computer WKS-014

# 4. The classic summary: which GPOs applied vs were filtered out, and why.
gpresult /h C:\temp\gpresult.html /f
gpresult /scope computer /r

gpresult (and the RSoP report) shows Applied GPOs, Denied GPOs with the reason, and the winning GPO for any setting — the ground truth. The denial reasons and what each means:

Denied reason in gpresult/RSoP Meaning Confirm Fix
Access Denied (Security) No Apply permission for this principal Get-GPPermission Grant GpoApply to the right group
WMI Filter (false) The filter query returned no results Run the WQL on the client Fix/loosen the filter
Disabled Link The link is present but disabled Get-GPInheritance Enable the link
Empty GPO has no settings in that half Report shows no extension Remove the dead link or add settings
Not Applied (Blocked) Block Inheritance stopped it Get-GPInheritance block state Enforce the GPO or remove the block
Denied (loopback) User GPO not on the computer’s OU gpresult /scope user Re-link the user GPO

Modes of RSoP

RSoP has two modes, and knowing which you need saves confusion. Logging mode (gpresult /r, or a live Get-GPResultantSetOfPolicy) reports what actually applied to a real, existing user+computer — the tool for post-change verification and incident triage. Planning mode (GPMC’s “Group Policy Modeling”) simulates the result for a hypothetical user/computer/OU/site — the tool for a pre-change “what if” before you touch anything. Use planning to predict, logging to confirm.

Also read the client operational log when a refresh misbehaves — it carries per-CSE timing that the GPMC link list never shows:

# Group Policy operational events, including per-extension timing.
Get-WinEvent -LogName "Microsoft-Windows-GroupPolicy/Operational" -MaxEvents 50 |
    Select-Object TimeCreated, Id, LevelDisplayName, Message | Format-Table -Wrap

The Group Policy operational event IDs worth alerting on:

Event ID Meaning Signals
4001 GP processing started (user/computer) Refresh cycle boundary
4016 / 5016 CSE started / completed with timing Per-extension duration — find slow CSEs
5312 List of applied GPOs What actually won
6017 / 7017 CSE error / warning A failing extension (scripts, drive maps)
1085 A CSE failed to apply a setting Partial-apply root cause
1129 Network not ready (slow link) Foreground async / boot-time issues

Cross-domain migration with migration tables

Promoting a GPO across domains (dev→prod, or post-M&A) needs Import-GPO plus a migration table to rewrite domain-specific references — UNC paths, security principals, DOMAIN\group names — so a GPO built in dev.contoso.com lands correctly in corp.contoso.com.

# Import into the target domain using a migration table, mapping by friendly name.
Import-GPO -BackupGpoName "C-BASE-AllComputers-SecurityBaseline" `
    -TargetName    "C-BASE-AllComputers-SecurityBaseline" `
    -Path          "C:\GPOBackups\2026-04-14_020000" `
    -MigrationTable "C:\migrations\corp.migtable" `
    -CreateIfNeeded

A migration table (.migtable) is XML mapping source → destination for three reference kinds. Generate a starter table from a backup, then edit it in GPMC’s Migration Table Editor:

# Generate a migration table skeleton from a backup (lists every principal/UNC found).
$gpm = New-Object -ComObject GPMgmt.GPM
$constants = $gpm.GetConstants()
$mt = $gpm.CreateMigrationTable()
$backupDir = $gpm.GetBackupDir("C:\GPOBackups\2026-04-14_020000")
$search = $gpm.CreateSearchCriteria()
$backups = $backupDir.SearchBackups($search)
foreach ($b in $backups) { $mt.Add(0, $b) }   # populate from settings + ACL
$mt.Save("C:\migrations\corp.migtable")

The three reference kinds and how to map each:

Reference kind Example source Mapping options Recommended
Local/UNC path \\dev\share\logon.bat Same / relative name / explicit / none Explicit to the prod UNC
Free-text principal DEV\Domain Users Same / map by relative name / explicit / none Relative name if the group exists in both
SID-based principal S-1-5-21-... (a dev group SID) Same / relative name / explicit / none Explicit to the prod SID

Each mapping has four option semantics: Copy identically keeps the source value verbatim (only when it is genuinely valid in the destination, which is rare cross-domain); Map by relative name rewrites DEV\GG-XPROD\GG-X when the same group name exists in both domains; Explicitly map points a source at a specific destination value when names, SIDs, or UNCs differ (the common case); and None drops the reference when it should not exist in prod at all.

Without a migration table, Import-GPO imports references literally. A GPO referencing dev.contoso.com\GG-LabVMs lands that exact broken string in prod, and a UNC logon-script path points back at the dev server. Always import cross-domain with a reviewed table — and make the table itself a reviewed artifact in Git.

Advanced GPO auditing

“Who changed what and when” must be answerable without git blame alone — because someone can still edit live in GPMC. Two layers of native auditing make every change attributable.

Directory Service Changes (the AD half)

GPO metadata (the GPC, links, permissions, WMI-filter associations) lives in AD, so Directory Service Changes auditing captures modifications to those objects. Enable it via the advanced audit policy — itself delivered by a GPO:

# Enable "Audit Directory Service Changes" (Success) on DCs via auditpol (set this in the DC baseline GPO).
auditpol /set /subcategory:"Directory Service Changes" /success:enable
# You must also set a SACL on the Policies container so changes are recorded:
#   dsacls "CN=Policies,CN=System,DC=corp,DC=contoso,DC=com" /S /A

SYSVOL file auditing (the settings half)

The actual settings live in SYSVOL files. Object Access → File System auditing with a SACL on the Policies SYSVOL folder records who wrote to a GPT.

# Enable file-system auditing (set in the DC/file-server baseline GPO).
auditpol /set /subcategory:"File System" /success:enable
# Then place a SACL on the SYSVOL Policies folder (audit Write/Delete for Everyone).
$path = "C:\Windows\SYSVOL\domain\Policies"
$acl  = Get-Acl $path -Audit
$rule = New-Object System.Security.AccessControl.FileSystemAuditRule(
    "Everyone", "Write,Delete", "ContainerInherit,ObjectInherit", "None", "Success")
$acl.AddAuditRule($rule)
Set-Acl $path $acl

The auditing layers and the event IDs they produce:

Layer Audit subcategory SACL on Key event IDs Answers
AD object change Directory Service Changes CN=Policies,CN=System 5136, 5137, 5141 GPC/link/ACL edits
SYSVOL settings File System (Object Access) SYSVOL Policies 4663, 4656 Who wrote a settings file
GP client apply (client operational log) n/a 4016/5016, 5312 What applied on a machine
Version bump — (compare GPC vs GPT) n/a GPC/GPT version mismatch

Correlate the audit trail with the Git history so the two systems of record agree:

Question Native audit source Git source Cross-check
Who edited GPO X? 5136 (Directory Service Changes) git log reports/X.xml Author matches operator
What exactly changed? 4663 (SYSVOL write) + report git diff on the report Diff explains the setting
Was it reviewed? PR approver An edit with no PR = out-of-band
Can we roll it back? Restore from backup git revert + Import-GPO Both restore the same state

Architecture at a glance

Picture the system as three planes that a change flows through left to right: a source-of-truth plane, a validation-and-review plane, and a runtime plane on the domain — with a feedback loop that pulls live drift back into source.

On the left is the source-of-truth plane: the Git repository. It holds five kinds of artifact per GPO — the Backup-GPO output under backups/, the diff-friendly XML report under reports/, the security-filtering CSV under permissions/, and the two things a backup misses, links.csv and wmi-filters.csv. Alongside sits the versioned ADMX for the Central Store and the SCT baselines with their LGPO text. Nothing is authoritative here except the repo: the live domain is treated as a deployment target, not the record.

In the middle is the validation-and-review plane: the CI pipeline. A pull request that changes any artifact triggers Validate-Gpo.ps1, which fails the build on the anti-patterns this article enumerates — a name that breaks the <Scope>-<Layer>-<Target>-<Purpose> convention, a WMI filter using Win32_Product, a GPO with no computer-readable Read (the MS16-072 landmine), an empty or unlinked GPO. In parallel, the pipeline renders the XML diff of the changed reports and posts it as a PR comment, so a second engineer reviews the actual settings delta, not a vague “updated the baseline”. Only when lint passes and a human approves does the change proceed.

On the right is the runtime plane: the domain itself. An approved change is applied first to a staging scope — a test OU or a staging domain — where Import-GpoEstate.ps1 recreates settings, replays links from links.csv, re-associates WMI filters, and replays permissions. Then the pipeline verifies: it forces a refresh on a canary object and captures RSoP (Get-GPResultantSetOfPolicy / gpresult), comparing the before/after effective result and reading per-CSE timing from the Group Policy operational log. A clean verification gates the promotion step, where a migration-table Import-GPO carries the change into production, rewriting devprod principals and UNCs as it lands. Underneath the whole runtime plane, native auditing — Directory Service Changes on CN=Policies and File System SACLs on SYSVOL — records every write, so even an out-of-band GPMC edit is attributable.

The feedback loop closes the design: a scheduled Export-GpoEstate.ps1 on a hardened admin host snapshots the live domain back into the repo and commits any drift. If someone edited live, the next snapshot surfaces it as an un-reviewed commit that stands out against the PR history — turning “someone changed something in GPMC” from an invisible risk into a visible diff. Read the whole thing as one sentence: changes flow repo → CI → staging → verify → promote, while drift flows domain → snapshot → repo, and precedence (LSDOU, Enforced, filtering, loopback) governs what any given object actually gets at the runtime end.

Real-world scenario

A 40,000-seat manufacturing client — call them Meridian Components — had a 110-second average logon on shop-floor thin clients and no version control on a GPO estate that had grown for a decade. The environment was the usual swamp: ~280 GPOs, most kitchen-sink, nearly all scoped to Authenticated Users, a single Default Domain Policy carrying dozens of unrelated settings, and no answer to the auditor’s standing question about change control on the domain’s audit and password policy. Two admins edited live in GPMC; when a change broke something, root-cause took a full day of clicking.

The presenting problem was the logon time, but the real problem was that nothing was diagnosable because nothing was diffable. The breakthrough came from per-CSE timing in the operational log, not the GPMC link list. Two offenders dominated. First, a single AllSettings GPO linked at the domain carried drive-map and printer Preferences with no item-level targeting — every client processed every department’s drive maps. Second, roughly half the GPOs used a WMI filter joining against Win32_Product to gate “is application X installed”; because Win32_Product triggers an MSI self-consistency check on enumeration, every policy refresh was effectively running msiexec reconciliation on each client. On a thin client that alone was 40+ seconds.

The constraint was brutal: they could not re-OU 40,000 machines mid-quarter, and the drive maps genuinely had to differ by department. So the fix was surgical and staged through a new Git pipeline we stood up first — because we refused to make another unreviewed live change on an estate this fragile. We exported the entire estate (Backup-GPO -All, XML reports, links.csv, wmi-filters.csv, permissions) into a repo, added Validate-Gpo.ps1 to CI, and only then began changing GPOs through pull requests.

# The offender hunt that started it: every WMI filter referencing Win32_Product.
Get-CimInstance -Namespace "root\Policy" -ClassName "MSFT_SomFilter" |
    Where-Object { $_.Query -match "Win32_Product" } |
    Select-Object Name, Query

The remediation: split the monolith into a C-BASE baseline plus U-ROLE Preferences GPOs; move department logic into item-level targeting inside one Preferences GPO instead of many separate ones; and rip out every Win32_Product filter, replacing the “is app X installed” checks with registry-presence ILT (which costs nothing). Every one of those changes went through CI — the Win32_Product lint rule would now fail the build if anyone reintroduced it. We also flipped the shop-floor thin clients to loopback Replace so per-user processing collapsed to one predictable set. Logon dropped from 110 seconds to 22 seconds.

The audit problem solved itself as a side effect: with Directory Service Changes and SYSVOL file auditing enabled (delivered by a new DC baseline GPO), and every intentional change now a reviewed PR, the auditor got exactly what they’d asked for — attributable, reviewed change control. The timeline of what mattered, and why the order was the lesson:

Phase Action Effect Why this order
Week 0 Export estate to Git, add CI lint No change to prod yet Never touch a fragile estate un-reviewed
Week 1 Find Win32_Product filters via operational-log timing Root cause identified The link list never showed this
Week 2 Split monolith → C-BASE + U-ROLE, via PRs Reviewed, reversible CI blocks reintroducing the offender
Week 2 Move dept logic to ILT Drive maps still per-dept, no extra GPOs Precision without OU explosion
Week 3 Loopback Replace on thin clients Per-user processing collapses Predictable kiosk experience
Week 3 Enable DS-Changes + SYSVOL auditing Auditor requirement met Attribution for out-of-band edits
Result 110 s → 22 s logon; full change control Diffable estate = diagnosable estate

The lasting lesson the team internalised: Win32_Product and untargeted Preferences are silent logon taxes you find in per-CSE timing, not the link list — and an estate you cannot diff is an estate you cannot diagnose.

Advantages and disadvantages

Managing Group Policy as code is overwhelmingly the right call at any scale past a handful of GPOs, but it is not free — it trades click-ops immediacy for pipeline discipline. Weigh it honestly:

Advantages Disadvantages
Every change is a reviewed PR — a second engineer sees the actual settings delta before it applies Requires pipeline discipline; a team used to live GPMC edits must change habits
Full history and attribution via git blame + native auditing — root-cause in minutes, not a day Two systems of record (Git + live domain) can drift if the snapshot job lapses
CI catches the classic breaks (MS16-072, Win32_Product, empty/unlinked GPOs) before they ship Backup-GPO misses links/WMI filters — you must build and maintain the extra export/replay scripts
Reproducible cross-domain promotion via Import-GPO + migration table — no manual re-creation Migration tables are fiddly; a wrong mapping silently imports a broken reference
One-command rollback: git revert + Import-GPO restores a known-good state “Restore” reproduces settings only — you must replay links/filters to make it live
Baselines measurable against SCT via PolicyAnalyzer — drift becomes a build signal SCT baselines evolve per OS release; you own the merge of Microsoft’s changes into your estate
The repo doubles as audit evidence — change control an auditor accepts Local GPO (LGPO) is a separate mechanism from domain GPO — two tools, two habits

When each side matters: the advantages compound with scale and regulation — at 40,000 seats or under audit, the review gate and attribution are non-negotiable, and the pipeline pays for itself the first time it blocks an MS16-072 break. The disadvantages bite hardest on small teams with no CI habit and on estates that adopt the snapshot job but never the apply discipline, so live edits keep sneaking in. The pragmatic path is the hybrid model: adopt the snapshot/audit pipeline immediately (zero disruption, instant history), then move changes onto the GitOps apply path GPO by GPO as the team’s discipline matures.

Hands-on lab

Stand up the core of a GPO-as-code pipeline in a lab domain: create a layered GPO, back up the estate, generate a diff-friendly report, initialise a Git repo, run the validation script, break a rule and watch CI catch it, then restore. Run on a domain-joined admin host (or a DC in a lab) with RSAT and the GroupPolicy module. This is non-destructive to a lab; do not run the “break a rule” step against production.

Step 1 — Prerequisites and a working folder.

Import-Module GroupPolicy, ActiveDirectory
$repo = "C:\git\gpo-repo-lab"
New-Item -ItemType Directory -Path $repo,"$repo\backups","$repo\reports","$repo\permissions" -Force | Out-Null
Set-Location $repo
git init | Out-Null

Expected: the module loads without error and the folder structure exists.

Step 2 — Create a correctly-named layered GPO and a rule-breaking one.

New-GPO -Name "C-BASE-AllComputers-LabBaseline"  -Comment "Lab baseline (correct name)" | Out-Null
New-GPO -Name "BadlyNamedTestGPO"                 -Comment "Deliberately wrong name"     | Out-Null
# Put a real setting in the good one so it isn't empty.
Set-GPRegistryValue -Name "C-BASE-AllComputers-LabBaseline" `
    -Key "HKLM\Software\Policies\Microsoft\Windows\Personalization" `
    -ValueName "NoLockScreen" -Type DWord -Value 1 | Out-Null

Expected: two GPOs created; Get-GPO -All | Select DisplayName lists both.

Step 3 — Export the estate (settings, reports, permissions).

Backup-GPO -All -Path "$repo\backups" | Out-Null
Get-GPO -All | ForEach-Object {
    $safe = $_.DisplayName -replace '[\\/:*?"<>|]', '_'
    Get-GPOReport -Guid $_.Id -ReportType Xml -Path "$repo\reports\$safe.xml"
    Get-GPPermission -Guid $_.Id -All |
        Select-Object @{n='Trustee';e={$_.Trustee.Name}}, Permission, Inherited |
        Export-Csv "$repo\permissions\$safe.csv" -NoTypeInformation
}

Expected: reports\ now contains one XML per GPO, including BadlyNamedTestGPO.xml.

Step 4 — Commit the baseline state.

git add -A
git commit -m "Initial GPO estate snapshot (lab)" | Out-Null
git log --oneline

Expected: one commit listed — your estate is now versioned.

Step 5 — Run the validation script and watch it FAIL on the bad name. Save the Validate-Gpo.ps1 from the CI section into $repo\scripts\, then:

& "$repo\scripts\Validate-Gpo.ps1" -RepoRoot $repo
$LASTEXITCODE   # expect 1 (failure)

Expected output includes: Naming: 'BadlyNamedTestGPO' violates <Scope>-<Layer>-<Target>-<Purpose> and an exit code of 1 — this is CI catching the anti-pattern before it could ship.

Step 6 — Fix the violation, re-validate, and confirm it passes.

Rename-GPO -Name "BadlyNamedTestGPO" -TargetName "C-ROLE-Lab-TestSetting"
# Re-export the reports so the repo reflects the fix.
Get-GPO -All | ForEach-Object {
    $safe = $_.DisplayName -replace '[\\/:*?"<>|]', '_'
    Get-GPOReport -Guid $_.Id -ReportType Xml -Path "$repo\reports\$safe.xml"
}
Remove-Item "$repo\reports\BadlyNamedTestGPO.xml" -ErrorAction SilentlyContinue
& "$repo\scripts\Validate-Gpo.ps1" -RepoRoot $repo
$LASTEXITCODE   # expect 0 (pass)

Expected: GPO validation passed. and exit code 0.

Step 7 — Verify effective state, then restore to prove reversibility.

# Effective state check (run against a lab computer object you have).
gpresult /r /scope computer

# Prove restore works: delete a setting, then restore from the Step 3 backup.
Set-GPRegistryValue -Name "C-BASE-AllComputers-LabBaseline" `
    -Key "HKLM\Software\Policies\Microsoft\Windows\Personalization" `
    -ValueName "NoLockScreen" -Type DWord -Value 0 | Out-Null
Restore-GPO -Name "C-BASE-AllComputers-LabBaseline" -Path "$repo\backups"
(Get-GPRegistryValue -Name "C-BASE-AllComputers-LabBaseline" `
    -Key "HKLM\Software\Policies\Microsoft\Windows\Personalization" `
    -ValueName "NoLockScreen").Value   # back to 1

Expected: the value is restored to 1 — the backup round-tripped the setting.

Validation checklist and teardown. You created a layered GPO, versioned the estate, watched CI catch a naming violation and pass after the fix, verified effective state with gpresult, and proved a restore round-trips settings. What each step proved:

Step What you did What it proves
2 Named GPO by convention + a bad one The convention is machine-checkable
3–4 Export + commit The estate becomes versioned, diffable
5 Validation fails on bad name CI blocks anti-patterns before apply
6 Fix + re-validate passes The gate is fixable, not just a blocker
7 gpresult + restore Effective-state truth + one-command rollback

Teardown (removes only the lab GPOs and folder):

Remove-GPO -Name "C-BASE-AllComputers-LabBaseline" -ErrorAction SilentlyContinue
Remove-GPO -Name "C-ROLE-Lab-TestSetting"          -ErrorAction SilentlyContinue
Remove-Item $repo -Recurse -Force

Common mistakes & troubleshooting

The playbook you bookmark — every “why isn’t my policy applying” (or “why is logon slow”) failure mode, its root cause, the exact command to confirm, and the fix.

# Symptom Root cause Confirm (exact cmd / path) Fix
1 GPO looks correct in GPMC but applies to nobody Computer lost Read after user-only security filtering (MS16-072) Get-GPPermission -Name <gpo> -All — no Domain Computers/Auth Users Read Re-grant GpoRead to Domain Computers (keep Apply on the user group)
2 Restored GPO does nothing Backup carried settings only — no links/filters Get-GPInheritance -Target <OU> shows no link Replay New-GPLink from links.csv; re-associate WMI filter
3 Logon takes 60–110 s Win32_Product WMI filter triggers MSI self-repair every refresh Get-CimInstance -Namespace root\Policy MSFT_SomFilter | ? Query -match Win32_Product Replace with registry-presence ILT; delete the filter
4 Setting is wrong; unclear which GPO set it Precedence — a lower/enforced GPO wins gpresult /h out.html /f → “Winning GPO” Re-order links or remove the enforced conflict
5 Kiosk lockdown not applying Loopback set in user half, or user GPO not on the computer’s OU gpresult /scope user /r on the kiosk Set loopback in the computer GPO; link the U- GPO to the computer’s OU
6 Admins also locked down on kiosks Loopback Replace with no exclusion RSoP for an admin logon Security-filter the lockdown to exclude the admin group
7 Two admins see different available settings No Central Store — each uses local ADMX Check \\<domain>\SYSVOL\...\PolicyDefinitions exists Create the Central Store; seed from newest OS
8 Cross-domain import references broken groups/UNCs Imported without a migration table Report shows DEV\... in prod Re-import with a reviewed .migtable
9 GPO edits not attributable to a person AD/SYSVOL auditing not enabled auditpol /get /subcategory:"Directory Service Changes" = No Auditing Enable DS-Changes + SYSVOL File System audit via the DC baseline
10 Client applies stale/partial settings GPC (AD) and GPT (SYSVOL) version mismatch after replication lag Get-GPO <gpo> | Select Computer/UserVersion; compare AD vs SYSVOL Force SYSVOL/DFS-R convergence; re-check replication
11 Preferences drive maps apply to everyone No item-level targeting on the preference items Inspect the Preferences GPO ILT tab Add ILT (group/subnet/OS conditions) per item
12 Enforced/Block sprawl hides precedence Routine use of Enforced/Block Inheritance The Enforced/Block audit script (above) Redesign OUs; delete redundant enforcements
13 A GPO is linked but “Empty” in RSoP Settings live in the disabled half Get-GPO <gpo> | Select GpoStatus Enable the right half or remove the dead link
14 WMI filter “false” everywhere Query bug or wrong class/namespace Run the WQL locally: Get-CimInstance -Query "<wql>" Fix the query; prefer Win32_OperatingSystem
15 Import-GPO fails “backup not found” Wrong -BackupGpoName or path (GUID vs name) Check manifest.xml in the backup root Use the exact display name from the manifest

The entries that bite hardest, expanded:

1. MS16-072 — the correct GPO that applies to nobody. After you filter a GPO’s Apply to a user group and remove Authenticated Users, the GPO is retrieved in the computer’s context and the computer can no longer read it. GPMC shows the GPO as perfectly configured; gpresult shows it Denied (Access Denied). Confirm: Get-GPPermission -Name <gpo> -All and look for any of Domain Computers, Domain Controllers, or Authenticated Users with GpoRead/GpoApply. Fix: re-grant GpoRead (Read-only) to Domain Computers — keep GpoApply on the actual user target. This one pattern prevents the most common post-patch Group Policy outage in existence.

2. Restore that does nothing. Restore-GPO/Import-GPO reproduce settings but not links or WMI-filter associations. The GPO comes back correct and unlinked. Confirm: Get-GPInheritance -Target <OU> shows the GPO absent from the link list. Fix: replay links from your links.csv and re-associate the WMI filter — which is exactly why you exported them as separate artifacts.

3. The Win32_Product logon tax. Confirm: the one-liner in the table finds every offending filter. Fix: the “is app installed” question is far cheaper as a registry-presence check in item-level targeting (Preferences) or a Win32Reg_AddRemovePrograms-style query — never Win32_Product.

10. GPC/GPT version mismatch. A GPO edited on one DC hasn’t fully replicated (AD via normal replication, SYSVOL via DFS-R) so a client reads a new GPC version pointing at an old GPT (or vice-versa) and applies partial settings. Confirm: compare the version on the GPO object with the SYSVOL GPT.ini; dcdiag/repadmin for the underlying replication health. Fix: let replication converge (or force it) before expecting the change; if SYSVOL replication itself is broken, that is a DFS-R problem to solve first.

Best practices

Security notes

Group Policy is a privileged control channel: whoever can edit a linked GPO can push code (startup scripts run as SYSTEM), change the security baseline, or weaken the firewall on every machine in scope. Treat the whole pipeline as Tier-0.

The privilege model at a glance:

Capability Grants effectively Restrict to Delivered/checked by
Edit a linked GPO Code execution as SYSTEM on scoped machines GG-GPO-Admins (small) Set-GPPermission -PermissionLevel GpoEdit
Link a GPO to an OU Apply policy to that OU’s objects OU-delegated admins Delegation on the OU
Write to SYSVOL Policies Edit any GPO’s settings files GPO admins + DCs only SACL + folder ACL
Edit the Central Store Change available settings for all editors GPO admins SYSVOL ACL + Git review
Approve a GPO PR Ship a policy change Reviewers (branch policy) CI + branch protection

Cost & sizing

Group Policy itself is free — it ships with AD DS — so “cost” here is operational, not licensing: the engineering effort to build and run the pipeline, and the compute for the automation host. The trade is real but small against the incident time it removes.

Cost driver Rough magnitude Notes
Pipeline build (one-off) 3–5 engineer-days Export/import scripts, CI lint, repo layout
Automation/admin host ~₹2,000–4,000/mo (a small VM) or reuse a PAW Runs the scheduled snapshot + CI apply
Git hosting ₹0 (self-hosted / free tier) to modest The repo is text + small backups
Ongoing maintenance ~0.5 day/month Update ADMX, SCT baselines, review drift
SCT / LGPO / PolicyAnalyzer ₹0 Free Microsoft downloads
Central Store ₹0 (SYSVOL space) A few MB of ADMX/ADML

Sizing is about estate hygiene, not spend:

Estate signal Healthy target Why
GPOs per domain As few as the design needs (layered) Every extra GPO is refresh cost + cognitive load
Settings per GPO One concern Predictability; disable the unused half
WMI filters Minimal, Win32_OperatingSystem-based Each runs on every machine every refresh
Enforced links A handful, documented Each hides true precedence
Snapshot frequency Hourly–daily Fast drift detection vs commit noise
Backup retention in Git Full history (it’s small) Free rollback to any prior state

The return: at 40,000 seats, cutting logon from 110 s to 22 s across two logons/day/user is on the order of thousands of person-hours a year recovered — before counting the incident time the diff/attribution removes. The pipeline pays for itself the first time CI blocks an MS16-072 break that would otherwise have been a fleet-wide outage.

Interview & exam questions

1. Explain LSDOU and how link order interacts with it. Group Policy applies Local → Site → Domain → OU (parent OU down to the object’s OU); later overrides earlier per setting. Within one container, link order decides precedence, where the lowest order number is processed last and therefore wins. So a deep OU beats a shallow one beats the domain beats the site beats local. (Maps to AZ-800/AZ-801 and MD-102 identity/GP objectives.)

2. A GPO is filtered to a user group and stops applying after patching. Why? MS16-072: since June 2016, GPOs are retrieved in the computer’s security context, so a GPO the computer can’t Read won’t apply even to a targeted user. Fix by re-granting Domain Computers (or Authenticated Users) Read while keeping Apply on the user group.

3. What does Backup-GPO capture, and what does it miss? It captures the GPO’s settings (both GPC and GPT halves) and a report. It does not capture links (they live on the OU/site/domain gPLink), WMI-filter definitions, or the filter-to-GPO association — so a restore recreates an unlinked, unfiltered GPO unless you script those separately.

4. When do you use loopback Merge vs Replace? Loopback (in a computer GPO on the machine’s OU) drives user policy from the machine. Merge applies the user’s normal GPOs then the computer-OU user GPOs (latter wins) — corporate shared desktops. Replace ignores the user’s own user GPOs entirely and applies only the computer-OU user settings — kiosks/RDS where everyone gets an identical shell.

5. Why avoid Win32_Product in a WMI filter? Enumerating Win32_Product triggers an MSI self-consistency check (a msiexec reconciliation) for every installed product, on every policy refresh, on every machine in scope — adding tens of seconds to logon at scale. Use registry-presence checks (item-level targeting) instead.

6. How do you promote a GPO from a dev domain to prod correctly? Import-GPO with a migration table that maps domain-specific references — UNC paths, free-text principals, and SID-based principals — from source to destination. Without it, references import literally and break (e.g. DEV\GG-X lands in prod). The migration table itself should be a reviewed artifact.

7. What is the Central Store and why does it matter? A single PolicyDefinitions (ADMX/ADML) folder in SYSVOL that every GPMC editor uses, overriding each admin’s local templates. It guarantees all editors see the same available settings regardless of their OS/patch level, preventing unreadable-reference drift. Seed it from the newest patched OS you manage.

8. How would you make Group Policy changes attributable and reviewable? Put GPO backups + XML reports + links/filters/permissions under Git as the system of record; gate changes with a CI lint (naming, MS16-072, Win32_Product, empty/unlinked) and a human PR review; and enable native auditing (Directory Service Changes on CN=Policies, File System SACL on SYSVOL) so even out-of-band edits are logged.

9. Enforced vs Block Inheritance — what wins? Enforced (on a link) beats Block Inheritance (on an OU) every time: an enforced GPO cannot be blocked and wins over lower links. That’s why security baselines are enforced top-down — no OU can escape them by blocking inheritance.

10. A setting is applying with the wrong value. How do you find which GPO set it? Run gpresult /h report.html /f (or an RSoP logging report) — it names the winning GPO for every setting and lists Denied GPOs with reasons. That’s ground truth; the GPMC link list only shows what’s considered, not what won.

11. What’s the difference between RSoP logging and planning mode? Logging mode (gpresult, live Get-GPResultantSetOfPolicy) reports what actually applied to a real user+computer — for verification and triage. Planning mode (Group Policy Modeling) simulates the result for a hypothetical user/computer/OU/site — for “what if” before you make a change.

12. How do you manage policy on non-domain-joined or pre-join machines as code? Use LGPO.exe from the Security Compliance Toolkit to import/export local GPO as text you can commit, review, and diff, and apply Microsoft’s downloadable security baselines; PolicyAnalyzer diffs a baseline against live policy so drift is measurable.

Quick check

  1. Within a single OU with three linked GPOs, which link order number wins, and why?
  2. You filter a GPO’s Apply to GG-Finance and remove Authenticated Users. What one permission must you keep for the GPO to still apply, and to whom?
  3. Name two things Backup-GPO does not capture.
  4. For a kiosk where every user must get the identical locked shell regardless of who logs on, which loopback mode do you use and in which half of which GPO?
  5. What single WMI class should you never use in a filter, and what does it do to logon?

Answers

  1. Link order 1 wins — the lowest number is processed last in that container, so it has the highest precedence and overwrites conflicting settings from higher-numbered links.
  2. Keep Read (GpoRead) for the computerDomain Computers (or Authenticated Users). Since MS16-072 the GPO is retrieved in the computer’s context, so without computer Read it applies to nobody, even the targeted users.
  3. Any two of: links (gPLink on OU/site/domain), WMI-filter definitions, and the filter-to-GPO association. (Settings are captured; these three are not.)
  4. Replace mode, configured in the computer half of a GPO linked to the computer’s OU (the user settings it applies must be reachable from that computer’s OU chain).
  5. Win32_Product — enumerating it triggers an MSI self-consistency check (a msiexec reconciliation) for every installed product on every refresh, adding tens of seconds to logon at scale.

Glossary

Next steps

Group PolicyGPOWindows ServerPowerShellActive DirectoryGitOpsLGPOSecurity Compliance Toolkit
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading