Wes Ellis./ a personal notebook
Technology. Stories. Side projects.
A few things worth writing down.
← Back to Script Library

SCRIPT LIBRARY · POWERSHELL

Forcing a Group Policy Refresh with PowerShell and SCCM

A gpupdate wrapper that checks the domain trust first, never bounces anyone's session, and reports a real success or failure back to ConfigMgr.

AT A GLANCEInvoke-GroupPolicyRefresh.ps1
What it does
Confirms the device is domain-joined and its secure channel is healthy, runs gpupdate for the computer, user or both, answers "No" to any logoff or reboot prompt, and returns the exit code and the tail of the output.
Requires
  • Windows PowerShell 5.1 (Test-ComputerSecureChannel is 5.1 only; the check is skipped on PowerShell 7)
  • A domain-joined device
Permissions
Local administrator or SYSTEM for computer policy. User policy has to run as the signed-in user.
Runs on
Windows 10/11, Windows Server 2016+
Tested
Parse-checked and dry-run with mocked gpupdate, CIM and secure channel calls in PowerShell 7.4

You've just changed a GPO, the change is needed now, and the default refresh interval is 90 minutes plus a random offset of up to 30. So you want to kick a refresh on a group of machines, and you want to know which ones actually took it.

The old version of this post was mostly ConfigMgr packaging code wrapped around a one-line gpupdate /force. That line did the job on a good day. On a bad day it could sit waiting for a "log off now?" answer nobody was there to give, and it reported success no matter what gpupdate said.

This version does the checks I'd do by hand first. Is the machine on the domain at all? Can it still talk to a domain controller over its secure channel? Then it runs the refresh without letting it log anyone off, and passes gpupdate's exit code back so the ConfigMgr deployment status tells the truth.

Invoke-GroupPolicyRefresh.ps1Download
<#
.SYNOPSIS
    Forces a Group Policy refresh on the local device and reports whether it worked.
.DESCRIPTION
    Checks that the device is domain-joined and still trusts the domain, then runs
    gpupdate /force (or a lighter changes-only refresh) for the target you choose, answering "No" to any logoff or reboot
    prompt so nothing gets bounced mid-afternoon. Returns an object with the exit code
    and the tail of gpupdate's output, and exits non-zero on failure so ConfigMgr
    reports it correctly.
.PARAMETER Target
    Computer, User or Both. User policy only makes sense when the script runs as the
    signed-in user, not as SYSTEM.
.PARAMETER WaitSeconds
    How long gpupdate waits for policy processing to finish before returning.
.PARAMETER ChangedOnly
    Skip /force and apply only settings that changed. Much lighter on domain
    controllers when you're hitting a whole collection.
.PARAMETER SkipSecureChannelCheck
    Don't test the computer's trust relationship with the domain first.
.EXAMPLE
    .\Invoke-GroupPolicyRefresh.ps1
.EXAMPLE
    .\Invoke-GroupPolicyRefresh.ps1 -Target Both -WaitSeconds 300 -WhatIf
#>
[CmdletBinding(SupportsShouldProcess)]
param(
    [ValidateSet('Computer', 'User', 'Both')]
    [string]$Target = 'Computer',
    [ValidateRange(0, 3600)]
    [int]$WaitSeconds = 120,
    [switch]$ChangedOnly,
    [switch]$SkipSecureChannelCheck
)

$result = [pscustomobject]@{
    ComputerName  = $env:COMPUTERNAME
    Target        = $Target
    DomainJoined  = $null
    SecureChannel = $null
    ExitCode      = $null
    Result        = $null
    Output        = $null
}

$system = Get-CimInstance -ClassName Win32_ComputerSystem
$result.DomainJoined = [bool]$system.PartOfDomain
if (-not $result.DomainJoined) {
    $result.Result = 'Skipped (not domain-joined)'
    return $result
}

if ($Target -ne 'Computer' -and [Security.Principal.WindowsIdentity]::GetCurrent().IsSystem) {
    Write-Warning 'Running as SYSTEM, so the user half of this refresh applies to SYSTEM, not to whoever is signed in.'
}

# A broken trust relationship makes gpupdate fail in confusing ways. Check it up front.
if (-not $SkipSecureChannelCheck -and (Get-Command -Name Test-ComputerSecureChannel -ErrorAction SilentlyContinue)) {
    try { $result.SecureChannel = Test-ComputerSecureChannel -ErrorAction Stop }
    catch { $result.SecureChannel = $false; Write-Verbose "Secure channel test failed: $($_.Exception.Message)" }

    if (-not $result.SecureChannel) {
        $result.Result = 'Failed (no secure channel to the domain)'
        Write-Error 'This computer cannot talk to a domain controller over its secure channel. Fix the trust relationship first.'
        $result
        exit 1
    }
}

$arguments = @("/wait:$WaitSeconds")
if (-not $ChangedOnly) { $arguments = @('/force') + $arguments }
if ($Target -ne 'Both') { $arguments = @("/target:$($Target.ToLower())") + $arguments }

if (-not $PSCmdlet.ShouldProcess($env:COMPUTERNAME, "gpupdate $($arguments -join ' ')")) {
    $result.Result = 'WhatIf'
    return $result
}

Write-Verbose "Running gpupdate $($arguments -join ' ')"
# Pipe "N" so a policy that wants a logoff or reboot doesn't get one.
$output = 'N' | & gpupdate.exe @arguments 2>&1
$result.ExitCode = $LASTEXITCODE
$result.Output   = (@($output | ForEach-Object { "$_".Trim() } | Where-Object { $_ }) | Select-Object -Last 4) -join ' | '
$result.Result   = if ($result.ExitCode -eq 0) { 'Success' } else { 'Failed' }

$result
if ($result.ExitCode -ne 0) { exit $result.ExitCode }

Parameters

ParameterTypeDefaultWhat it's for
-TargetstringComputerComputer, User or Both. Running as SYSTEM, only Computer really means anything.
-WaitSecondsint120How long gpupdate waits for processing to finish before handing control back. Policy keeps applying in the background after that.
-ChangedOnlyswitch—Drop /force and apply only what changed. Use this when you're refreshing a big collection.
-SkipSecureChannelCheckswitch—Don't test the trust relationship first. Handy on a VPN that's slow to come up, where you'd rather let gpupdate try anyway.

Run it

Refresh computer policy on this machine.

.\Invoke-GroupPolicyRefresh.ps1

See exactly what gpupdate command it would run.

.\Invoke-GroupPolicyRefresh.ps1 -Target Both -WhatIf

A lighter refresh across a whole collection, as a ConfigMgr program.

powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\Invoke-GroupPolicyRefresh.ps1 -ChangedOnly

Hit a few machines remotely and see which ones failed.

Invoke-Command -ComputerName PC-0142, PC-0187 -FilePath .\Invoke-GroupPolicyRefresh.ps1 | Where-Object Result -ne 'Success'

What you'll see

Example outputvalues are illustrative
ComputerName  : PC-0142
Target        : Computer
DomainJoined  : True
SecureChannel : True
ExitCode      : 0
Result        : Success
Output        : Updating policy... | Computer Policy update has completed successfully.

How it works

  1. Is it even on the domain? Win32_ComputerSystem.PartOfDomain answers that. Workgroup machines are skipped and marked as such, instead of showing up as failures.
  2. Can it reach a DC? On Windows PowerShell 5.1, Test-ComputerSecureChannel checks the machine account's trust with the domain. A broken trust is the classic reason gpupdate throws vague errors, so it's worth catching up front.
  3. Build the command. /target:computer or /target:user (or neither, for both), plus /force unless you asked for -ChangedOnly, plus /wait.
  4. Run it without the drama. The script pipes N into gpupdate, so if a policy wants a logoff or restart, the answer is no. Standard output and errors are captured together.
  5. Tell the truth. The last few lines of output go into the result object, and a non-zero gpupdate exit code becomes the script's exit code. ConfigMgr marks the deployment failed on the machines where it actually failed.

For a quick refresh on a handful of machines, ConfigMgr's Run Scripts is the easiest route: approve the script once and run it against a collection straight from the console. A package with a program still makes sense if you want it scheduled or repeated.

One habit from the old version I've dropped on purpose: deleting and recreating the ConfigMgr package every time the script ran. That wipes the deployment history and breaks existing deployments. Update the package content instead.

Take it further

  • Check what actually applied. gpresult /r /scope computer on one of the machines shows the GPOs that applied, and the ones that were filtered out and why.
  • Use the GPMC for an OU. Right-click an OU in Group Policy Management and choose Group Policy Update. It uses the same remote task trick as Invoke-GPUpdate and needs the remote scheduled task firewall rules open.
  • Log it. Pipe the results to Export-Csv from an Invoke-Command run and you've got a record of which machines had the new policy, and when.

Things that'll trip you up

  • SYSTEM doesn't have your user's policy. A ConfigMgr program runs as SYSTEM, so a user refresh from there refreshes SYSTEM's user policy, which isn't what you meant. For user settings, deploy it to run with the user's rights, or use Invoke-GPUpdate from the GroupPolicy module, which schedules a task in each signed-in user's session.
  • Some settings still need a logoff or reboot. Software installation, folder redirection and a few others only apply at startup or sign-in. The script answers "No" to the prompt on purpose, so those wait for the next natural restart instead of bouncing someone mid-call.
  • /force on 2,000 machines at once hurts. Every machine re-downloads and re-applies everything, all at the same domain controllers. Use -ChangedOnly for big collections, or stagger the deployment.
  • Off-network machines will fail, and that's accurate. No domain controller, no policy. A failure on a laptop that's at home without VPN is the right answer, not a bug.
  • A broken trust relationship stops here. If the secure channel test fails, the script stops and says so. Rejoining the domain or running Test-ComputerSecureChannel -Repair on the machine is the fix, not another gpupdate.