Manage group policies with PowerShell
Despite Intune, DSC and Entra ID (Azure AD); group policies are still a widely used method to manage Windows clients and servers in many companies. However, the MMC is not really suitable for managing several hundred GPOs. But that’s what PowerShell is designed for.
Most people will have already used Get-GPO
, but Microsoft provides 25 more cmdlets and two aliases in the GroupPolicy
module.
PS C:\Users\Fabian> Get-Command -Module GroupPolicy
CommandType Name Version Source
----------- ---- ------- ------
Alias Get-GPPermissions 1.0.0.0 GroupPolicy
Alias Set-GPPermissions 1.0.0.0 GroupPolicy
Cmdlet Backup-GPO 1.0.0.0 GroupPolicy
Cmdlet Copy-GPO 1.0.0.0 GroupPolicy
Cmdlet Get-GPInheritance 1.0.0.0 GroupPolicy
Cmdlet Get-GPO 1.0.0.0 GroupPolicy
Cmdlet Get-GPOReport 1.0.0.0 GroupPolicy
Cmdlet Get-GPPermission 1.0.0.0 GroupPolicy
Cmdlet Get-GPPrefRegistryValue 1.0.0.0 GroupPolicy
Cmdlet Get-GPRegistryValue 1.0.0.0 GroupPolicy
Cmdlet Get-GPResultantSetOfPolicy 1.0.0.0 GroupPolicy
Cmdlet Get-GPStarterGPO 1.0.0.0 GroupPolicy
Cmdlet Import-GPO 1.0.0.0 GroupPolicy
Cmdlet Invoke-GPUpdate 1.0.0.0 GroupPolicy
Cmdlet New-GPLink 1.0.0.0 GroupPolicy
Cmdlet New-GPO 1.0.0.0 GroupPolicy
Cmdlet New-GPStarterGPO 1.0.0.0 GroupPolicy
Cmdlet Remove-GPLink 1.0.0.0 GroupPolicy
Cmdlet Remove-GPO 1.0.0.0 GroupPolicy
Cmdlet Remove-GPPrefRegistryValue 1.0.0.0 GroupPolicy
Cmdlet Remove-GPRegistryValue 1.0.0.0 GroupPolicy
Cmdlet Rename-GPO 1.0.0.0 GroupPolicy
Cmdlet Restore-GPO 1.0.0.0 GroupPolicy
Cmdlet Set-GPInheritance 1.0.0.0 GroupPolicy
Cmdlet Set-GPLink 1.0.0.0 GroupPolicy
Cmdlet Set-GPPermission 1.0.0.0 GroupPolicy
Cmdlet Set-GPPrefRegistryValue 1.0.0.0 GroupPolicy
Cmdlet Set-GPRegistryValue 1.0.0.0 GroupPolicy
Find group policies
Unfortunately Get-Gpo
does not support any search filter but can only output all or one GPO. You can specify individual group policies via -Id
or -Name
, but you must know the exact values. With the parameter -All
a list of all GPOs is output.
A filter can therefore always be applied only afterwards.
# Default Domain Policy
Get-Gpo -Id 31b2f340-016d-11d2-945f-00c04fb984f9
# Default Domain Controllers Policy
Get-Gpo -Id 6ac1786c-016f-11d2-945f-00c04fb984f9
# List all GPOs
Get-Gpo -All
# Filter by name
Get-Gpo -All | Where-Object DisplayName -match "Default Domain"
Create a group policy
To create a new, empty group policy you do not need to provide much information. A descriptive name is sufficient, a descriptive comment is desirable.
PS C:\Users\Fabian> New-GPO -Name "MSFT Windows Server 2022 - Member Server" -Comment "Microsoft Security Baseline"
DisplayName : MSFT Windows Server 2022 - Member Server
DomainName : lab.bader.cloud
Owner : LAB\Domain Admins
Id : 0ef52ff6-0ef0-42e1-9273-508463a69a44
GpoStatus : AllSettingsEnabled
Description : Microsoft Security Baseline
CreationTime : 11/15/2021 8:07:55 PM
ModificationTime : 11/15/2021 8:07:55 PM
UserVersion : AD Version: 0, SysVol Version: 0
ComputerVersion : AD Version: 0, SysVol Version: 0
WmiFilter :
Import settings
In the case of security baselines, for example, exports of group policies are often supplied as templates. Microsoft also does this with its Microsoft Security Compliance Toolkit. This contains the Microsoft recommended settings for all common operating systems and browsers.
To import these templates Import-GPO
can be used. This cmdlet overwrites all existing settings in the group policy.
$GPO = Get-Gpo -Name "MSFT Windows Server 2022 - Member Server"
Import-GPO -Path "C:\Users\Fabian\Downloads\Windows Server-2022-Security-Baseline-FINAL\GPOs" -BackupGpoName "MSFT Windows Server 2022 - Member Server" -TargetGuid $GPO.id
Instead of creating the GPO beforehand, this can be done in one step with the -CreateIfNeeded
parameter.
Import-GPO -Path "C:\Users\Fabian\Downloads\Windows Server-2022-Security-Baseline-FINAL\GPOs" -BackupGpoName "MSFT Windows Server 2022 - Domain Controller" -TargetName "MSFT Windows Server 2022 - Domain Controller" -CreateIfNeeded
When importing a GPO, a mapping file can also be provided. This translates e.g. SIDs from one domain to the other.
Import-GPO -Path "C:\Users\Fabian\Downloads\Windows Server-2022-Security-Baseline-FINAL\GPOs" -BackupGpoName "MSFT Windows Server 2022 - Domain Controller" -TargetName "MSFT Windows Server 2022 - Domain Controller" -CreateIfNeeded -MigrationTable C:\Users\Fabian\Documents\DomainA2DomainB.migtable
The required .migtable
file must be created manually. It is recommended to use the GUI and save the mapping for future use. If you don’t want to do this you can easily create the file by hand. It is an XML file as shown in this example.
<?xml version="1.0" encoding="utf-16"?>
<MigrationTable xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.microsoft.com/GroupPolicy/GPOOperations/MigrationTable">
<Mapping>
<Type>Computer</Type>
<Source>COMPUTER01</Source>
<Destination>COMPUTER02</Destination>
</Mapping>
<Mapping>
<Type>User</Type>
<Source>Administrator</Source>
<Destination>SuperAdmin</Destination>
</Mapping>
</MigrationTable>
Create a report
Similar to the GUI, the configuration settings of a group policy can be exported. However, different formats can be selected.
- HTML = The report known from the MMC that can be viewed in the browser.
- XML = Output in XML format. This makes it possible e.g. to process the information with PowerShell.
Get-GPOReport -Name "MSFT Windows Server 2022 - Member Server" -ReportType Html -Path .\Desktop\Report.html
Get-GPOReport -Name "MSFT Windows Server 2022 - Member Server" -ReportType Xml -Path .\Desktop\Report.xml
Get-GPOReport
is unfortunately not very performant, so I can’t recommend the cmdlet for reports over many group policies.Create and modify OU links
For a GPO to be applied in the first place, it must be linked to one or more OUs, domains or sites. New-GPLink
does this reliably. If you want to adjust this link afterwards, this is easily possible with Set-GPLink
. This makes e.g. the adjustment of the link order much easier.
$GPO = Get-Gpo -Name "MSFT Windows Server 2022 - Member Server"
# Create a new link
New-GPLink -Guid $GPO.Id -Target "OU=DemoOU,$((Get-ADDomain).DistinguishedName)" -LinkEnabled Yes -Order 1
# Change existing link
Set-GPLink -Guid $GPO.Id -Target "OU=DemoOU,$((Get-ADDomain).DistinguishedName)" -LinkEnabled No
Enforced
parameter the settings of this group policy will always be applied regardless of the “Link Order” or interrupted inheritance.Set-GPLink -Guid $GPO.Id -Target "OU=DemoOU,$((Get-ADDomain).DistinguishedName)" -Enforced Yes
Remove a link
It is just as easy to remove such a link again.
Remove-GPLink -Guid $GPO.Id -Target "OU=DemoOU,$((Get-ADDomain).DistinguishedName)"
Determine assigned group policies
In order to find out which GPOs are effective at a certain OU, Get-GPInheritance
is used.
Get-GPInheritance "OU=DemoOU,$((Get-ADDomain).DistinguishedName)"
Name : demoou
ContainerType : OU
Path : ou=demoou,dc=lab,dc=bader,dc=cloud
GpoInheritanceBlocked : No
GpoLinks : {}
InheritedGpoLinks : {Default Domain Policy}
Block inheritance
Set-GPInheritance "OU=DemoOU,$((Get-ADDomain).DistinguishedName)" -IsBlocked Yes
Name : demoou
ContainerType : OU
Path : ou=demoou,dc=lab,dc=bader,dc=cloud
GpoInheritanceBlocked : Yes
GpoLinks : {}
InheritedGpoLinks : {}
Enforced
flag enabled also affects the objects below this OU regardless if inheritance is blocked.Assign WMI filters
The creation of WMI filters is unfortunately not possible via PowerShell with native cmdlets. Of course, the community has stepped up and provided a corresponding module. GPWmiFilter by Friedrich Weinmann.
What is natively possible, however, is if a WMI is already in use, to assign it to another GPO.
# Get WMI filter from existing GPO
$WMIFilter = Get-GPO -Name "SourceGPO" | Select-Object -ExpandProperty WmiFilter
# Get target GPO ...
$GPO = Get-GPO -Name "MSFT Windows Server 2022 - Member Server"
# ... an apply the WMI filter
$GPO.WmiFilter = $WMIFilter
Read permissions
To retrieve the permissions assigned to a group policy, Get-GPPermission
is used. It is possible to read all permissions, or only for a named group or AD object.
Get-GPPermission -Name "MSFT Windows Server 2022 - Member Server" -All
Trustee : Authenticated Users
TrusteeType : WellKnownGroup
Permission : GpoApply
Inherited : False
Trustee : Domain Admins
TrusteeType : Group
Permission : GpoEditDeleteModifySecurity
Inherited : False
Trustee : Enterprise Admins
TrusteeType : Group
Permission : GpoEditDeleteModifySecurity
Inherited : False
Trustee : ENTERPRISE DOMAIN CONTROLLERS
TrusteeType : WellKnownGroup
Permission : GpoRead
Inherited : False
Trustee : SYSTEM
TrusteeType : WellKnownGroup
Permission : GpoEditDeleteModifySecurity
Inherited : False
Get-GPPermission -Name "MSFT Windows Server 2022 - Member Server" -TargetType Group
Trustee : Authenticated Users
TrusteeType : WellKnownGroup
Permission : GpoApply
Inherited : False
Set permissions
If a group policy is only to be assigned to a specific group of people, for example, the authorization of the Authenticated Users must always be changed. This group must not be removed under any circumstances.
The default mode of the cmdlet Set-GPPermission
is additive, the named permissions are appended. If higher permissions already exist, they will remain. Therefore the correct use of the -Replace
parameter is important.
$GPO = Get-Gpo -Name "MSFT Windows 11 - User"
Set-GPPermission -Guid $GPO.Id -PermissionLevel GpoApply -TargetType Group -TargetName "Windows 11 Users"
Set-GPPermission -Name $GPO.Id -PermissionLevel GpoRead -TargetType Group -TargetName "Authenticated Users" -Replace
The same cmdlet is also used when permissions are to be removed. Here the PermissionLevel
is set to None and the parameter -Replace
is used.
Set-GPPermission -Guid $GPO.Id -PermissionLevel None -TargetName "Windows 11 Users" -TargetType Group -Replace
Backup
A backup of the group policies is very simple with Backup-GPO
. A new directory with a GUID is created for each backup and all relevant data is stored in this directory. A regular backup of all GPOs is thus no problem.
Get-GPO -All | Backup-GPO -Path C:\GPOBackup\ -Comment "$(Get-Date)"
Get values
To retrieve individual values from a GPO there are two cmdlets: Get-GPPrefRegistryValue
and Get-GPRegistryValue
.
The difference between the two cmdlets is the type of registry value they read from the group policy.
Get-GPPrefRegistryValue reads values created under Computer -> Preferences -> Windows Settings -> Registry. These are not the values that are controlled in the GUI via ADMX files, but values that are added manually. They are not limited to HKEY_LOCAL_(MACHINE|USER)\SOFTWARE\Policies\
.
Get-GPRegistryValue
is everything that is stored in the registry.pol
via an ADMX file. In most cases this is the area you want to edit.
By specifying the registry -Key
all values in this key can be retrieved.
Get-GPRegistryValue -Guid $GPO.Id -Key "HKLM\SOFTWARE\Policies\Microsoft\Windows\WinRM\Service\"
KeyPath : SOFTWARE\Policies\Microsoft\Windows\WinRM\Service
FullKeyPath : HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WinRM\Service
Hive : LocalMachine
PolicyState : Set
Value : 0
Type : DWord
ValueName : AllowBasic
HasValue : True
KeyPath : SOFTWARE\Policies\Microsoft\Windows\WinRM\Service
FullKeyPath : HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WinRM\Service
Hive : LocalMachine
PolicyState : Set
Value : 0
Type : DWord
ValueName : AllowUnencryptedTraffic
HasValue : True
KeyPath : SOFTWARE\Policies\Microsoft\Windows\WinRM\Service
FullKeyPath : HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WinRM\Service
Hive : LocalMachine
PolicyState : Set
Value : 1
Type : DWord
ValueName : DisableRunAs
HasValue : True
Get-GPRegistryValue -Guid $GPO.Id -Key "HKLM\SOFTWARE\Policies\Microsoft\Windows\WinRM\Service\" -ValueName AllowBasic
KeyPath : SOFTWARE\Policies\Microsoft\Windows\WinRM\Service
FullKeyPath : HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WinRM\Service
Hive : LocalMachine
PolicyState : Set
Value : 0
Type : DWord
ValueName : AllowBasic
HasValue : True
Set values
By using Set-GPRegistryValue
single values in the GPO can also be changed. Thus for setting new preferences the MMC is no longer necessary, but only the knowledge in which registry key which setting is hidden.
For this the web page admx.help is a big help. For nearly all ADMX files the appropriate values are available and the necessary information about “Registry Path " and Value Name/Type and possible values are broken down.
The following is an example of the setting Turn off real-time protection
Set-GPRegistryValue -Name 'MSFT Windows Server 2022 - Member Server' -Key 'HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows Defender\Real-Time Protection' -ValueName 'DisableRealtimeMonitoring' -Type DWord -Value 0
Get values recursive
Since it is not possible to read more than one level with the native cmdlet Get-GPRegistryValue
, I created a function inspired by this Snippet that enables you to do this. This way all settings of a group policy can be output as PolicyRegistrySetting objects and processed directly. This is much faster than the cmdlet Get-GPOReport
.
function Get-GPRegistryValueRecurse {
[CmdletBinding()]
param (
[Alias('DisplayName')]
[Parameter(Mandatory = $true,
ValueFromPipelineByPropertyName = $true)]
[string]$Name,
[Parameter(Mandatory = $true)]
[string]$Key
)
# Remove trailing backslash
$Key = $Key -replace '\\$'
$RegistryItems = Get-GPRegistryValue -Name $Name -Key $key
Write-Verbose "Found $($RegistryItems.Count) items"
foreach ($RegistryItem in $RegistryItems) {
if ( $RegistryItem -is [Microsoft.GroupPolicy.PolicyRegistrySetting] ) {
# Output the registry item
$RegistryItem
Write-Verbose "Found registry item: $($RegistryItem.Name)"
} elseif ( $RegistryItem -is [Microsoft.GroupPolicy.RegistryItem]) {
Write-Verbose "Found registry key: $($RegistryItem.FullKeyPath)"
# Go deeper
Get-GPRegistryValueRecurse -Key $RegistryItem.FullKeyPath -Name $Name
}
}
}
Example application with pipelining
$GPO | Get-GPRegistryValueRecurse -Key "HKLM\SOFTWARE\Policies\Microsoft\Windows\WinRM\Client\"
Merge group policies
With what we have just learned, another problem can be easily solved: How to merge two group policies into one?
Since the GUI only offers the import, but this function overwrites all existing settings, the only solution is to use the PowerShell.
In the following example I first recursively read all values below the registry key HKLM\SOFTWARE\Policies\Microsoft\Windows Defender\
to then transfer them via Set-GPRegistryValue
into the already existing GPO “MSFT Windows Server 2022 - Member Server”.
# Retrieve all settings from a specific registry key
$SourceSettings = Get-GPRegistryValueRecurse -Name "MSFT Windows Server 2022 - Defender Antivirus" -Key "HKLM\SOFTWARE\Policies\Microsoft\Windows Defender\"
# Define target GPO name
$TargetGPOName = "MSFT Windows Server 2022 - Member Server"
foreach ($Setting in $SourceSettings) {
if (-not [string]::isNullOrWhitespace($Setting ) ) {
# Replace double backslash if present
$TargetKey = $Setting.FullKeyPath -replace '\\\\', '\'
if ($Setting.PolicyState -eq "Delete") {
Set-GPRegistryValue -Name $TargetGPOName -Key $TargetKey -ValueName $($Setting.ValueName) -Disable
} else {
Set-GPRegistryValue -Name $TargetGPOName -Key $TargetKey -ValueName $($Setting.ValueName) -Type $($Setting.Type) -Value $($Setting.Value)
}
}
}
However, not all values, but only values that conflict with the source GPO.
This example can be very easily customized to include all “computer” values.
$SourceSettings = Get-GPRegistryValueRecurse -Name "MSFT Windows Server 2022 - Defender Antivirus" -Key "HKLM\SOFTWARE\"
Edit firewall settings
To edit the Windows Defender firewall settings, none of the above mentioned commands are suitable. However, Microsoft has two more cmdlets, or rather functions, up its sleeve.
PS C:\Users\Fabian> Get-Command *gpo* | ? Source -ne GroupPolicy
CommandType Name Version Source
----------- ---- ------- ------
Function Open-NetGPO 2.0.0.0 NetSecurity
Function Save-NetGPO 2.0.0.0 NetSecurity
Application chgport.exe 10.0.22... C:\Windows\system32\chgport.exe
Open-NetGPO
creates a WMI session to an existing group policy and then allows to use this session with the normal cmdlets to manage firewall rules. The WMI session must always be passed using the -GPOSession
parameter.
-GPOSession
is no longer available in the Windows 11 version of Get-NetFirewallRule
. -PolicyStore
can be used alternatively.New-GPO -Name "GPO_Server_Windows_Firewall"
$gpoSession = Open-NetGPO -PolicyStore lab.bader.cloud\GPO_Server_Windows_Firewall
# Windows until 11
Get-NetFirewallRule -GPOSession $gpoSession
# Windows 11
Get-NetFirewallRule -PolicyStore $gpoSession
# Add outbound DNS queries to 8.8.8.8
New-NetFirewallRule -GPOSession $gpoSession -DisplayName "Allow traffic to Google" -Action Allow -Direction Outbound -RemoteAddress 8.8.8.8 -Protocol Udp -RemotePort 53
# Save changes to GPO
Save-NetGPO -GPOSession $gpoSession
# Check if changes where made correctly
$gpoSession = Open-NetGPO -PolicyStore lab.bader.cloud\GPO_Server_Windows_Firewall
Get-NetFirewallRule -PolicyStore $gpoSession
Analyze all group policies
I kept encountering the following problem: In which group policy is one or more settings set and if so with which value.
The only answer I found so far on the Internet was to use Get-GPOReport
with the conversion to XML and subsequent evaluation of the XML files. Unfortunately this is very very slow, with several hundred, maybe even thousands of GPOs in the environment this is not a real solution.
Therefore I created the script CheckGPOSettings.ps1
.
This script analyzes all group policies in one or more domains for one or more values using a passed hashtable and returns the results as PowerShell objects. This makes it very easy to process them further. And best of all, it searches all typical locations within the GPO. No matter if Get-GPRegistryValue
, GptTmpl.inf
or Get-GPPrefRegistryValue
, the script finds them all.
audit.csv
are not supported.Query Hashtable
The hashtable used for the search consists of two nested hashtables. The outer one uses a freely selectable name for the searched value as key. Here, for example, the meaningful name of the setting should be used.
As value another Hashtable is used, this describes the value to be searched. As Key the Registry Key is used and as Value the Value Name of the Registry Item.
$CheckSettings = @{
"Unique setting name for the report. Choose something you can remember like the display name of the setting" = @{
# Key = The path to the registry key
"Key" = "HKLM\SOFTWARE\Policies\Microsoft\PassportForWork"
# ValueName = The name of the registry value
"ValueName" = "Enabled"
}
}
In the following example I am looking for two different registry settings. The first one is the LAN Manager Authentication Level (hopefully set to 5) and the second one is the value for the service MrxSmb10
better known as SMBv1. A value of 4 is desired (= Disabled).
$CheckSettings = @{
"Network security: LAN Manager authentication level" = @{
"Key" = "HKLM\System\CurrentControlSet\Control\Lsa"
"ValueName" = "LmCompatibilityLevel"
}
"SMBv1 service state" = @{
"Key" = "HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\MrxSmb10"
"ValueName" = "Start"
}
}
Now I can start the function Get-SettingsFromGPOs
using the domain and the created hashtable.
C:\Users\Fabian> Get-SettingsFromGPOs -Domain "lab.bader.cloud" -CheckSetting $CheckSettings
As a result I get the following information:
- In which domain does the group policy exist
- What is the name of the group policy
- Where is this group policy linked
- Is a WMI filter applied
- The path to the GPO
- Current status of the GPO (Enabled/Disabled)
- The custom name for the setting
- The registry key
- The name of the RegistryItem
- The value of the RegistryItem
- The type of the RegistryItem
DomainName : lab.bader.cloud
GPO : MSFT Windows Server 2022 - Domain Controller
GPOLinks :
Setting : Network security: LAN Manager authentication level
ValueName : LmCompatibilityLevel
Hive : HKLM\System\CurrentControlSet\Control\Lsa
WmiFilter :
PolicyState :
Value : 5
Type : REG_DWORD
Path : cn={1C09EB8B-1EB0-488B-A92C-34030213F58B},cn=policies,cn=system,DC=lab,DC=bader,DC=cloud
DomainName : lab.bader.cloud
GPO : MSFT Windows Server 2022 - Domain Controller
GPOLinks :
Setting : SMBv1 service state
ValueName : Start
Hive : HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\MrxSmb10
WmiFilter :
PolicyState : Set
Value : 4
Type : DWord
Path : cn={1C09EB8B-1EB0-488B-A92C-34030213F58B},cn=policies,cn=system,DC=lab,DC=bader,DC=cloud
* Currently I can only find out the GPLinks by using the cmdlet Get-GPReport
. But this is only started if the searched value is found in the group policy. For additional performance I added the switch -NoGPLink
. This improved the execution time from 776ms to 568ms in my small demo environment. So in particularly large environments, this can be worthwhile.
In this GitHub Repository you can find more examples and the repository is open for pull requests. So over time a collection of search parameters can be gathered.
Summary
With PowerShell, group policies can be manipulated very extensively. Unfortunately, it is not as easy to get started as with other cmdlets, but once you understand the basic concept, you can do a lot of things, fast.
If you have been paying attention, you will notice that some cmdlets did not make it into the blog article. This is either because they are self-explanatory (Copy-GPO
), the counterpart was introduced (Remove-GPRegistryValue
) or I just don’t use them (*-GPStarterGPO
).
Therefore examples for the following cmdlets are missing
Copy-GPO
Get-GPStarterGPO
Invoke-GPUpdate
New-GPStarterGPO
Remove-GPPrefRegistryValue
Remove-GPRegistryValue
Rename-GPO
Restore-GPO