Skip to content
06 · Group Policy & SYSVOLPart 1 of 4Foundation

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.

Florian Amette5 min read

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.

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

PowerShell
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 -AutoSize

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:

PowerShell
$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

PowerShell
$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

  1. 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.
  2. 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.
  3. Confirm removal:
PowerShell
Get-ChildItem -Path $sysvolPath -Recurse -Include *.xml -ErrorAction SilentlyContinue |
    Select-String -Pattern 'cpassword="[^"]+"' | Measure-Object

An 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.

PowerShell
$scriptsPath = "\\$((Get-ADDomain).DNSRoot)\SYSVOL\$((Get-ADDomain).DNSRoot)\scripts"

(Get-Acl -Path $scriptsPath).Access |
    Select-Object IdentityReference, FileSystemRights, AccessControlType

Expected 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.

PowerShell
# 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

Related guides

Group Policy & SYSVOL

Auditing GPO permissions and gPLink rights

Find who can edit, create and link GPOs in Active Directory: GPO ACLs, gPLink rights on OUs and sites, Group Policy Creator Owners and WMI filters.

Intermediate
Group Policy & SYSVOL

Finding and removing GPP passwords (cpassword)

Find every Group Policy Preferences cpassword in SYSVOL, backups and client caches, map it to the exposed account, rotate it and stop it coming back.

Foundation
Group Policy & SYSVOL

Deploying Microsoft security baselines with GPO

Deploy Microsoft security baselines with Group Policy: SCT, Policy Analyzer gap analysis, LGPO testing, ring-based rollout, exceptions and drift checks.

Intermediate