active directory · gpo · PowerShell
Managing Group Policy ACLs with PowerShell
The use case I needed to solve looked like this:
- A Group Policy already enforces some behaviour in the environment.
- I want to allow exceptions through a pull request on a GitHub repo.
- That keeps the approval in the PR, without needing to give everyone access to GPMC.
In this case, the exceptions are computer accounts. I’m adding a deny for the Apply Group Policy extended right on one GPO.
A quick scope check first: computer security filtering controls computer policy. My original wallpaper example is normally a user setting, so excluding a computer from that GPO isn’t by itself a general way to exempt the logged-on user. User filtering and loopback processing are separate considerations. Use a computer setting when testing this example.
The right we’re changing
We want the computer to keep reading the GPO but stop applying it. That means denying Apply Group Policy, not denying every permission on the object:
Apply Group Policy extended right
The schema GUID for that right is:
edacfd8f-ffb3-11d1-b41d-00a0c968f939
A simple exception file might look like this:
[
{
"computername": "bob-machine1",
"expires": "2022-01-01"
},
{
"computername": "bob-machine2",
"expires": "2022-01-01"
}
]
And the worker can translate those entries into explicit deny ACEs on the GPO:
$exceptionsFile = "C:\automation\repo-name\exceptions.json"
$targetGpoGuid = "bd782ff6-907e-4346-8452-1751957cb35e"
$domainDn = "DC=testdomain,DC=local"
$applyGroupPolicyRight = [Guid]"edacfd8f-ffb3-11d1-b41d-00a0c968f939"
$exceptions = @(Get-Content -Raw -Path $exceptionsFile -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop)
if ($exceptions.Count -lt 1 -or $exceptions.Count -gt 100) {
Write-Error "Number of exceptions is out of acceptable range. Count was $($exceptions.Count)"
exit 1
}
$gpo = Get-GPO -Guid $targetGpoGuid -ErrorAction Stop
$gpoAdsi = [ADSI]"LDAP://CN={$($gpo.Id)},CN=Policies,CN=System,$domainDn"
$acl = $gpoAdsi.ObjectSecurity
foreach ($computerEntry in $exceptions) {
$computer = Get-ADComputer $computerEntry.computername -ErrorAction Stop
$computerSid = [System.Security.Principal.SecurityIdentifier]$computer.SID
$ace = New-Object System.DirectoryServices.ActiveDirectoryAccessRule(
$computerSid,
"ExtendedRight",
"Deny",
$applyGroupPolicyRight
)
$acl.AddAccessRule($ace)
}
$gpoAdsi.ObjectSecurity = $acl
$gpoAdsi.CommitChanges()
The assignment back to $gpoAdsi.ObjectSecurity matters: we’ve changed the ACL object in memory, and now need to put it back on the directory entry before CommitChanges() persists it.
What this worker doesn’t do yet
This is the add operation, not a complete exception reconciler. The expires field in the JSON is metadata here; this excerpt doesn’t act on it. Deleting a computer from the file or letting that date pass won’t remove the deny ACE already written to AD. The surrounding job needs a matching removal operation for the entries it owns. Otherwise your six-month exception will still be working in three years.
I’d also make the DC explicit in the finished worker so the GPO lookup, LDAP write and verification all hit the same server. After the change, inspect the security filtering and use gpresult on the target computer to confirm the GPO is denied for the reason you intended. Reading the ACL alone doesn’t tell you which user or computer policy was processed.
One final gotcha: the explicit deny on this GPO also applies when the computer belongs to another group that allows it to apply. Adding an allow elsewhere in that same ACL won’t cancel the exception.