Group Policy and SYSVOL hardening
Lock down GPO delegation, purge Group Policy Preferences cpassword secrets from SYSVOL, and add change control to prevent silent GPO abuse.
Group Policy is one of the most powerful control planes in Active Directory — a GPO linked to the wrong OU, or an edit right delegated to the wrong group, can push a scheduled task or a startup script to every computer in scope. SYSVOL, the share that replicates GPO content to every domain controller, has its own long tail of legacy risk, most famously the cpassword field left behind by Group Policy Preferences. This guide covers delegation review, the cpassword cleanup, SYSVOL permissions, and change control, with PowerShell for each.
GPO delegation: who can edit and link
Two separate permissions matter and are often confused: edit rights on the GPO object itself (who can change what the policy does) and link rights on the OU or domain object (who can decide where it applies). A user with only edit rights on a GPO that's linked nowhere sensitive is low risk; a user with link rights on the Domain Controllers OU or the domain root is high risk regardless of which GPOs they can edit, because they can link an existing low-privilege-looking GPO that someone else has write access to.
Review current GPO permissions
Import-Module GroupPolicy
Get-GPO -All | ForEach-Object {
$gpo = $_
Get-GPPermission -Guid $gpo.Id -All |
Where-Object { $_.Permission -in 'GpoEditDeleteModifySecurity','GpoEdit' } |
Select-Object @{n='GPOName';e={$gpo.DisplayName}}, Trustee, Permission
} | Format-Table -AutoSizeReview who can link GPOs (gPLink write access)
Link rights live on the OU/domain object's ACL, not the GPO. Check the Domain Controllers OU and domain root specifically, since those are the highest-impact link targets:
$dcOU = (Get-ADDomain).DomainControllersContainer
$domainRoot = (Get-ADDomain).DistinguishedName
foreach ($target in @($dcOU, $domainRoot)) {
Write-Host "== $target ==" -ForegroundColor Cyan
(Get-Acl -Path "AD:\$target").Access |
Where-Object { $_.ObjectType -eq 'f30e3bbe-9ff0-11d1-b603-0000f80367c1' -or $_.ActiveDirectoryRights -match 'WriteProperty|GenericAll|GenericWrite' } |
Select-Object IdentityReference, ActiveDirectoryRights, AccessControlType
}The GUID f30e3bbe-9ff0-11d1-b603-0000f80367c1 is the gPLink attribute's schema GUID, so this filters to write access on the link attribute specifically, plus any broad grants that would cover it. Remove any trustee here that isn't Domain Admins or a specifically chartered Group Policy administration group — link rights on the Domain Controllers OU should not be delegated broadly.
The Group Policy Preferences cpassword problem
Before MS14-025 (KB2962486, released May 13, 2014), Group Policy Preferences let administrators push local account passwords, drive mappings with credentials, scheduled tasks, and services with stored passwords through GPOs, encrypting the password with a static AES key that Microsoft had published as part of the GPP documentation. Any authenticated domain user can read SYSVOL, so any cpassword value left behind is trivially decryptable by anyone with read access to the share — the encryption provides no real protection.
The patch stopped the GPP editor from writing new cpassword values, but did nothing to remove ones created earlier. This is the single most common finding in AD environments older than the patch.
Find cpassword values in SYSVOL
$sysvolPath = "\\$((Get-ADDomain).DNSRoot)\SYSVOL\$((Get-ADDomain).DNSRoot)\Policies"
Get-ChildItem -Path $sysvolPath -Recurse -Include *.xml -ErrorAction SilentlyContinue |
Select-String -Pattern 'cpassword="[^"]+"' |
Select-Object Path, LineNumber, @{n='Match';e={$_.Matches.Value}}GPP stores these in specific XML files depending on preference type — Groups.xml (local accounts), Services.xml, ScheduledTasks.xml, DataSources.xml, and Drives.xml are the common ones. The search above catches all of them since it scans every XML in the Policies tree.
Remediate
- For every hit, open the corresponding GPO in the Group Policy Management Console and remove the offending preference item (Local Users and Groups, Scheduled Tasks, Drive Maps, or Services, as applicable) rather than hand-editing the XML.
- Change the credential everywhere it was used. The cpassword being removed from the GPO does not rotate the actual password on the target systems — treat every discovered value as a compromised credential and rotate it.
- Confirm removal:
Get-ChildItem -Path $sysvolPath -Recurse -Include *.xml -ErrorAction SilentlyContinue |
Select-String -Pattern 'cpassword="[^"]+"' | Measure-ObjectAn empty result confirms no remaining cpassword entries. Re-run this check periodically — a restored backup or a re-imported legacy GPO can silently reintroduce one.
SYSVOL permissions and script review
SYSVOL also carries startup, shutdown, logon, and logoff scripts referenced by GPOs. Anyone with write access to the relevant script folder can modify code that runs with SYSTEM privileges (startup/shutdown scripts) across every targeted computer.
$scriptsPath = "\\$((Get-ADDomain).DNSRoot)\SYSVOL\$((Get-ADDomain).DNSRoot)\scripts"
(Get-Acl -Path $scriptsPath).Access |
Select-Object IdentityReference, FileSystemRights, AccessControlTypeExpected baseline is Administrators (which contains Domain Admins and Enterprise Admins), SYSTEM and CREATOR OWNER with Full Control, and Authenticated Users and Server Operators with Read & Execute only. Anything broader than that on a write permission is a finding. Also periodically diff script file hashes against a known-good baseline to catch unauthorized modification outside of normal change windows.
GPO backup and change control
Treat GPOs like infrastructure-as-code: back them up before changes, and diff changes over time rather than discovering them after the fact.
# Back up all GPOs to a dated folder
$backupPath = "D:\GPOBackups\$(Get-Date -Format yyyy-MM-dd)"
New-Item -Path $backupPath -ItemType Directory -Force | Out-Null
Backup-GPO -All -Path $backupPath
# Generate a full report for diffing against the previous baseline
Get-GPO -All | ForEach-Object {
Get-GPOReport -Guid $_.Id -ReportType Xml -Path "$backupPath\$($_.DisplayName -replace '[\\/:*?"<>|]','_').xml"
}Store the report exports in version control so changes to GPO settings, not just links, show up as reviewable diffs. Restore from a specific backup with Restore-GPO -Guid <id> -Path <backupPath> if a change needs to be rolled back.
What it breaks
- Restricting GPO edit/link delegation: any helpdesk or site-admin group that currently self-services GPO changes for their OU loses that ability and needs a formal request path, or a narrowly scoped delegation limited to their own OU (never the Domain Controllers OU or domain root).
- Removing cpassword-based GPP items: local accounts, scheduled tasks, or drive mappings that depended on that GPP item stop being configured centrally; you must replace the mechanism (LAPS for local admin passwords, a properly secured scheduled task deployment, or Group Policy drive maps without embedded credentials) before removing the old preference, not after.
- Tightening SYSVOL script folder permissions: breaks any workflow where non-admin staff currently drop or edit logon/startup scripts directly on the share instead of through change control.
- Locking gPLink rights to Domain Admins only: delegated regional or departmental GPO administrators who currently link their own GPOs will need Domain Admin involvement for linking going forward, even if they retain edit rights on their own GPOs.
For adjacent Tier 0 controls, see Domain controller hardening and Kerberos hardening.
Frequently asked questions
Is the Group Policy Preferences cpassword vulnerability still relevant?
Yes. MS14-025 stopped the GPP editor from creating new cpassword entries in 2014, but it did not retroactively remove any that already existed in SYSVOL. Domains that have been running since before 2014 frequently still have them sitting in old GPOs nobody has revisited.
Who should be allowed to link GPOs to the Domain Controllers OU?
Domain Admins only, in most environments. Linking a GPO to the Domain Controllers OU is equivalent to code execution on every DC, so gPLink write access there should be as tightly scoped as Domain Admin membership itself.
How do I find delegated GPO permissions without walking every GPO by hand?
Use Get-GPO combined with Get-GPPermission in a loop, or Get-GPOReport for an XML/HTML export you can diff over time. Both are built into the GroupPolicy PowerShell module and require no extra tooling.
Group Policy and SYSVOL hardening