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

SCRIPT LIBRARY · POWERSHELL

Re-Enable Windows Update with PowerShell: Find Every Registry Block, Not Just One

When Windows Update is greyed out or just won't run, check all the policy values and services that can block it, and put them back the way Windows shipped.

AT A GLANCERepair-WindowsUpdateAccess.ps1
What it does
Checks the registry policy values that disable Windows Update access or automatic updates, plus the Windows Update and BITS services. Removes anything that's blocking, resets disabled services to Manual, and returns one row per check.
Requires
  • Windows PowerShell 5.1 (PowerShell 7 works too)
  • No modules
Permissions
Local administrator or SYSTEM for the machine-wide checks. The optional per-user checks need to run as that user.
Runs on
Windows 10/11, Windows Server 2016+
Tested
Parse-checked and dry-run with mocked registry and service cmdlets in PowerShell 7.4

Part 3 of the thread Windows Update, untangled

You open Settings, and Windows Update is greyed out, or missing, or says "Some settings are managed by your organization" on a machine nobody manages. Usually one of three things happened: malware turned it off so it could stay put, a "debloat" script from the internet got a little enthusiastic, or a GPO from a previous life left something behind.

The original version of this post was a single line that set DisableWindowsUpdateAccess to 0. It's a real setting, but it's only one of several places Windows Update can be switched off, and Set-ItemProperty throws an error if the policy key doesn't exist in the first place.

This version checks all the usual suspects, shows you which ones are set, and removes them, so they go back to "Not configured" instead of being explicitly set to "allowed." It also looks at the two services that malware and debloat tools like to disable. Run it with -WhatIf first. The report is useful all by itself.

Repair-WindowsUpdateAccess.ps1Download
<#
.SYNOPSIS
    Finds and removes the registry settings that lock people (or Windows) out of Windows Update.
.DESCRIPTION
    Checks the handful of policy values that disable Windows Update access or automatic
    updates, plus the Windows Update and BITS services. Anything set to block is removed
    (returned to "Not configured"), and disabled services go back to Manual, which is how
    Windows ships them. Returns one object per check, so you can see what was wrong even
    when you only run it with -WhatIf.
.PARAMETER IncludeCurrentUser
    Also check the per-user policy values in HKCU. Only useful when run as the signed-in
    user, not as SYSTEM.
.PARAMETER KeepAutoUpdatePolicy
    Leave NoAutoUpdate alone. Some ConfigMgr and WSUS setups turn automatic updates
    off on purpose.
.PARAMETER SkipServices
    Leave service startup types alone.
.EXAMPLE
    .\Repair-WindowsUpdateAccess.ps1 -WhatIf
.EXAMPLE
    .\Repair-WindowsUpdateAccess.ps1 -IncludeCurrentUser -Verbose
#>
[CmdletBinding(SupportsShouldProcess)]
param(
    [switch]$IncludeCurrentUser,
    [switch]$KeepAutoUpdatePolicy,
    [switch]$SkipServices
)

$wuPolicy = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate'
$checks = @(
    @{ Path = $wuPolicy;       Name = 'DisableWindowsUpdateAccess'; Setting = 'Turn off access to all Windows Update features' }
    @{ Path = $wuPolicy;       Name = 'SetDisableUXWUAccess';       Setting = 'Remove access to use all Windows Update features' }
    @{ Path = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer';       Name = 'NoWindowsUpdate';            Setting = 'Windows Update removed from Start/Settings (machine)' }
    @{ Path = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\WindowsUpdate'; Name = 'DisableWindowsUpdateAccess'; Setting = 'Windows Update access disabled (machine)' }
)
if (-not $KeepAutoUpdatePolicy) {
    $checks += @{ Path = "$wuPolicy\AU"; Name = 'NoAutoUpdate'; Setting = 'Automatic Updates turned off' }
}
if ($IncludeCurrentUser) {
    $checks += @{ Path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer';       Name = 'NoWindowsUpdate';            Setting = 'Windows Update removed (user)' }
    $checks += @{ Path = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Policies\WindowsUpdate'; Name = 'DisableWindowsUpdateAccess'; Setting = 'Windows Update access disabled (user)' }
}

$changed = $false

foreach ($check in $checks) {
    $value = (Get-ItemProperty -Path $check.Path -Name $check.Name -ErrorAction SilentlyContinue).($check.Name)
    $row = [pscustomobject]@{
        Check    = $check.Setting
        Location = "$($check.Path)\$($check.Name)"
        Value    = $value
        Blocking = ($value -eq 1)
        Action   = 'None'
    }

    if ($row.Blocking) {
        if ($PSCmdlet.ShouldProcess($row.Location, 'Remove blocking policy value')) {
            try {
                Remove-ItemProperty -Path $check.Path -Name $check.Name -ErrorAction Stop
                $row.Action = 'Removed'
                $changed = $true
            }
            catch {
                $row.Action = 'Failed'
                Write-Warning "Couldn't remove $($row.Location): $($_.Exception.Message)"
            }
        }
        else { $row.Action = 'WhatIf' }
    }
    $row
}

if (-not $SkipServices) {
    foreach ($name in 'wuauserv', 'BITS') {
        $service = Get-Service -Name $name -ErrorAction SilentlyContinue
        $row = [pscustomobject]@{
            Check    = "$name service startup"
            Location = "Service:$name"
            Value    = if ($service) { "$($service.StartType)" } else { 'Missing' }
            Blocking = ($service -and "$($service.StartType)" -eq 'Disabled')
            Action   = 'None'
        }
        if ($row.Blocking) {
            if ($PSCmdlet.ShouldProcess($name, 'Set startup type to Manual')) {
                try {
                    Set-Service -Name $name -StartupType Manual -ErrorAction Stop
                    $row.Action = 'SetToManual'
                    $changed = $true
                }
                catch {
                    $row.Action = 'Failed'
                    Write-Warning "Couldn't change $name`: $($_.Exception.Message)"
                }
            }
            else { $row.Action = 'WhatIf' }
        }
        $row
    }
}

# Windows Update reads policy when it starts a scan, so give it a clean start.
if ($changed) {
    $wu = Get-Service -Name wuauserv -ErrorAction SilentlyContinue
    if ($wu -and $wu.Status -eq 'Running' -and $PSCmdlet.ShouldProcess('wuauserv', 'Restart service')) {
        Restart-Service -Name wuauserv -Force -ErrorAction SilentlyContinue
    }
    Write-Verbose 'Changes made. If a GPO or MDM policy set these values, it will set them again at the next refresh.'
}

Parameters

ParameterTypeDefaultWhat it's for
-IncludeCurrentUserswitch—Also check the per-user policy values in HKCU. Only meaningful when it runs as the signed-in user.
-KeepAutoUpdatePolicyswitch—Don't touch NoAutoUpdate. Use this on WSUS or ConfigMgr machines where automatic updates are off on purpose.
-SkipServicesswitch—Only look at the registry. Leave service startup types alone.

Run it

See what's blocking Windows Update, without changing anything.

.\Repair-WindowsUpdateAccess.ps1 -WhatIf

Show only the checks that found a problem.

.\Repair-WindowsUpdateAccess.ps1 -WhatIf | Where-Object Blocking

Fix everything, including the signed-in user's settings (run it in their session).

.\Repair-WindowsUpdateAccess.ps1 -IncludeCurrentUser

On a ConfigMgr-managed machine, where NoAutoUpdate may be deliberate.

.\Repair-WindowsUpdateAccess.ps1 -KeepAutoUpdatePolicy

What you'll see

Example outputvalues are illustrative
Check                                             Value    Blocking Action
-----                                             -----    -------- ------
Turn off access to all Windows Update features    1        True     Removed
Remove access to use all Windows Update features           False    None
Windows Update removed from Start/Settings (m...           False    None
Windows Update access disabled (machine)                   False    None
Automatic Updates turned off                      1        True     Removed
wuauserv service startup                          Disabled True     SetToManual
BITS service startup                              Manual   False    None

How it works

  1. Check each known blocker. The main ones live under HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate. DisableWindowsUpdateAccess comes from "Turn off access to all Windows Update features," and SetDisableUXWUAccess from "Remove access to use all Windows Update features." Then there's NoAutoUpdate in the AU subkey, plus the older Explorer-style NoWindowsUpdate and a machine-wide DisableWindowsUpdateAccess under CurrentVersion\Policies.
  2. Remove, don't flip. A value set to 1 gets deleted with Remove-ItemProperty. Deleting puts the setting back to "Not configured," which is how a clean install looks. Setting it to 0 would still be a policy, just a different one.
  3. Check the services. If wuauserv or BITS is disabled, it's set back to Manual. Both start on demand, and Manual is how Windows ships them.
  4. Report every check. You get a row for each value and service, blocking or not, so the -WhatIf run doubles as a diagnosis.
  5. Restart Windows Update if anything changed. The service reads policy at the start of a scan, so a clean restart means the next check for updates sees the new state.

Take it further

  • Kick off a scan. After a repair, UsoClient StartScan (or just clicking Check for updates) confirms the machine can actually reach its update source.
  • Check where it's pointed. A machine that was "fixed" by pointing it at a WSUS server that no longer exists is still broken. Look at WUServer and UseWUServer in the same policy key before you close the ticket.
  • Keep a baseline. Turn the report into a configuration item that flags any device with a blocking value, so you hear about it before the user does.

Things that'll trip you up

  • If a policy set it, the policy will set it again. Removing a value that a GPO or Intune profile owns only lasts until the next refresh. If it comes back, run gpresult /h report.html and find out which policy is doing it.
  • HKCU as SYSTEM is the wrong HKCU. Deployed through ConfigMgr or a scheduled task, the script runs as SYSTEM, and -IncludeCurrentUser checks SYSTEM's settings, not the user's. For per-user blocks, run it in the user's session.
  • Access denied on a service is a bad sign. Local admins can normally change wuauserv and BITS. If they can't, something has changed the service permissions, which is what malware tends to do. Scan the machine properly before you call it fixed.
  • It doesn't repair a broken Windows Update. This removes blocks. If updates still fail afterwards, the component store or the update agent itself may be damaged, and that's a job for DISM /RestoreHealth or an in-place upgrade.