By the end of this sheet, one ring’s servers are pulling updates directly from Microsoft instead of from WSUS01, verified twice: at the machine, where the policy values are provably gone and a forced scan reaches Microsoft’s endpoint, and in Update Manager, where a full cycle has run against the new source and left an installation record. Every other ring is untouched and still on WSUS. Rollback is one group membership change. Nothing in Azure is edited at any point.
Before step 1
The ring you are moving is already under a maintenance configuration and has completed one full patch cycle with WSUS still supplying the content, which is the exit criterion this series set for coexistence. Its machines are Arc connected, or are Azure virtual machines set to Customer Managed Schedules. You hold rights to create and link a Group Policy Object in the domain, and to edit the security group that filters the existing WSUS policy. You have local administrator on each server in the ring, and Reader plus Resource Graph query rights on the subscriptions holding the machine resources.
One honesty note, made once. This sheet is doc-verified rather than lab-run: every command is real and every setting is named as the product names it, but where a value is derived on your estate the sheet reads it instead of printing one.
Naming, decided once
The convention is class, scope, subject, state, so an object’s purpose survives its creator.
| Object | Name | Why this name |
|---|---|---|
| Domain global security group, filters the existing WSUS GPO | SEC-Srv-WSUS-Pinned | Its membership is the live list of machines still on WSUS. |
| Domain global security group, filters the successor GPO | SEC-Srv-WU-Direct | Its membership is the list of machines sourcing from Microsoft. |
| Successor Group Policy Object | GPO-Srv-WindowsUpdate-Direct | Names the end state, not the project. It carries update discipline and says nothing about a source. |
| Existing WSUS Group Policy Object | unchanged | Whatever it is called today, it is not edited, renamed or relinked at any point in this sheet. |
| Change record folder | cutover-Ring1-20260812 | Ring and date. Everything captured below lands here. |
Your WSUS GPO’s real name is an estate fact rather than a product noun, so find it rather than assuming. More than one object may write into the same key.
# Which GPO in the domain actually sets the WSUS pin? Read it, do not assume it.
Get-GPO -All | ForEach-Object {
$xml = Get-GPOReport -Guid $_.Id -ReportType Xml
if ($xml -match 'WUServer') { $_ | Select-Object DisplayName, Id }
}
# Expected: exactly one object. Two or more is a finding, and you resolve it before step 1.
What travels, and where each thing comes from
Five artefacts move into the change record. Capture each at the step that produces it; three cannot be produced again afterwards.
| File | Produced on | Purpose on arrival |
|---|---|---|
| Backup of the WSUS GPO, taken before anything moves | A domain controller, via Backup-GPO | Never edited, but a byte accurate copy is what lets you rebuild the pin if the group approach is abandoned. |
Backup of GPO-Srv-WindowsUpdate-Direct | A domain controller, via Backup-GPO | The new object is the one with no history. Back it up the moment it is correct. |
| Pre-cutover registry capture, one file per server | Each server in the ring | Holds that machine’s real WUServer value and full policy key contents. Rollback compares against this, not against memory. |
| Pre-cutover Update Manager record for the ring | Resource Graph, exported from the query in step 1 | The baseline for the source field, and the artefact that makes step 9’s unsettled question answerable on your estate. |
| Decoded Windows Update log, one per server | Each server, via Get-WindowsUpdateLog | Evidence of which endpoint the first post-cutover scan reached. Attach it, because the log is recycled. |
Step 1. Prove the ring has earned the cutover
This gate stands in front of the whole operation, and it is here rather than at the end because a ring that has not proven its schedule will fail after the cutover for reasons that look exactly like the cutover. Run the query first. If it comes back short, stop and give the ring another cycle.
patchinstallationresources
| where type has 'patchinstallationresults' and type !has 'softwarepatches'
| extend p = parse_json(properties)
| extend machineId = tostring(split(tolower(id), '/patchinstallationresults')[0])
| project machineId,
started = todatetime(p.startDateTime),
status = tostring(p.status),
source = tostring(p.patchServiceUsed),
installed = p.installedPatchCount,
failed = p.failedPatchCount,
overran = p.maintenanceWindowExceeded
| join kind=inner (
resources
| where type in~ ('microsoft.compute/virtualmachines','microsoft.hybridcompute/machines')
| where tags['PatchRing'] == 'Ring1'
| project machineId = tolower(id), machine = name
) on machineId
| project machine, started, status, source, installed, failed, overran
| order by started desc
Expected result: one row per machine in the ring, with status of Succeeded, a started timestamp inside the last thirty days, and overran false. A CompletedWithWarnings is usually a pending reboot and is not a pass. An overran of true means the window was too short for what the machine was offered, which gets worse once it is taking a full cumulative update from Microsoft, so fix the window before you fix the source. No rows at all means the run never happened, or happened more than thirty days ago, which is the retention limit on that table and the reason this evidence is exported rather than left in place.
Export the result. The source column is step 9’s baseline, and this is your only chance to record what the estate reports while the machines are demonstrably still on WSUS.
az extension add --name resource-graph # once per workstation
az graph query -q "$query" --first 100 --output json > .\cutover-Ring1-20260812\arg-baseline.json
Then capture the machine side on every server. The WSUS URL is an estate value, read into the capture rather than typed here.
# Save this as capture-wu-policy.ps1. Step 5 runs it again.
$pol = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate'
$au = Join-Path $pol 'AU'
foreach ($k in @($pol, $au)) {
if (Test-Path $k) {
(Get-Item $k).Property | ForEach-Object {
'{0}\{1} = {2}' -f $k, $_, (Get-ItemPropertyValue -Path $k -Name $_)
}
} else { "$k : key absent" }
}
# Expected now, before the cutover: WUServer and WUStatusServer under the parent key,
# UseWUServer = 1 under AU, plus whatever else your GPO carries.
# Save the output as <servername>-pre.txt in the change record folder.
Step 2. Build the successor policy before you take anything away
Create the object, name it, filter it, link it, and leave its group empty. It must be correct before a single machine leaves the WSUS group, because the gap between losing one policy and gaining the other is the window in which a server has a direct line to Microsoft and nothing restraining it.
In Group Policy Management, create a GPO named GPO-Srv-WindowsUpdate-Direct and link it to the organisational units holding your servers. Open it in the Group Policy Management Editor and go to Computer Configuration, Policies, Administrative Templates, Windows Components, Windows Update. On current administrative templates that node splits further, with Configure Automatic Updates under Manage end user experience and the WSUS location setting under Manage updates offered from Windows Server Update Service; on the older templates a 2016 estate may still carry, both sit directly under Windows Update.
| Setting | Value | Why |
|---|---|---|
| Configure Automatic Updates | Disabled | Writes NoAutoUpdate = 1 under ...\WindowsUpdate\AU, which is what Update Manager’s coexistence guidance asks for. The agent installs when Update Manager says so and at no other time. |
| Specify intranet Microsoft update service location | Not Configured | The successor policy must say nothing about a source. That is the entire point of the object. |
| Do not connect to any Windows Update Internet locations | Not Configured here, and removed wherever it is set | It applies only while a device is pointed at an intranet update service, so after the cutover it strands the machine. Look in your hardening baselines too. |
| Any Windows Update for Business deferral setting | Not Configured, permanently | The dual scan guard in step 6. Update Manager does not need deferral, and on the 2016 and 2019 machines here it creates a source you did not choose. |
| Any restart timing or restart deadline setting | Not Configured | Restart is the maintenance configuration’s decision, expressed as Reboot if required, Never reboot or Always reboot. A local restart policy can only be a second opinion, and Microsoft documents that a reboot can occur even under Never reboot. |
There is a genuine collision in that table, worth naming rather than papering over. Configure Automatic Updates is one three state setting, and the checkbox that opts a machine into updates for other Microsoft products lives inside its Enabled state, so setting it Disabled to get NoAutoUpdate = 1 puts that checkbox out of reach. Two honest resolutions exist: set it Disabled as above and carry the Microsoft Update opt in by service registration instead, which is step 7 and is what this sheet does, or set it Enabled with a notify only option so nothing installs unattended, accept that NoAutoUpdate is then 0, and tick the box.
Then filtering. On the Scope tab, remove Authenticated Users from Security Filtering and add SEC-Srv-WU-Direct. Authenticated Users includes computer accounts, so leaving it there applies the policy to every machine the link reaches, which is the estate wide flip this technique exists to avoid. Leave the group empty for now.
# Consolidated equivalent for step 2. Needs the GroupPolicy and ActiveDirectory modules.
$ou = 'OU=Servers,DC=catsnackjack,DC=com'
New-ADGroup -Name 'SEC-Srv-WU-Direct' -GroupScope Global -GroupCategory Security
$gpo = New-GPO -Name 'GPO-Srv-WindowsUpdate-Direct' -Comment 'Update discipline, no source.'
$au = @{
Name = $gpo.DisplayName
Key = 'HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU'
ValueName = 'NoAutoUpdate'
Type = 'DWord'
Value = 1
}
Set-GPRegistryValue @au
Set-GPPermission -Name $gpo.DisplayName -TargetName 'Authenticated Users' -TargetType Group -PermissionLevel None -Confirm:$false
Set-GPPermission -Name $gpo.DisplayName -TargetName 'SEC-Srv-WU-Direct' -TargetType Group -PermissionLevel GpoApply
New-GPLink -Name $gpo.DisplayName -Target $ou
# The backup folder name is generated, so read it rather than printing it.
$backup = Backup-GPO -Name $gpo.DisplayName -Path '\\fs01\changerecord\cutover-Ring1-20260812'
"Backup id: $($backup.Id)" # the folder name under the backup path
Failure mode to watch for: Set-GPRegistryValue writes straight into the object’s policy file, so a mistyped key or value name creates something no administrative template renders. Reopen the object in the editor afterwards and confirm Configure Automatic Updates shows as Disabled rather than as an unrecognised extra registry setting.
Step 3. Move the ring out of scope
Add first, remove second. In Active Directory Users and Computers, put the ring’s computer objects into SEC-Srv-WU-Direct, then take the same objects out of SEC-Srv-WSUS-Pinned. That order leaves a partial failure under both policies rather than under neither.
$ring = 'FS01','APP02','AZSRV02' # Ring1
Add-ADGroupMember -Identity 'SEC-Srv-WU-Direct' -Members ($ring | ForEach-Object { Get-ADComputer $_ })
Remove-ADGroupMember -Identity 'SEC-Srv-WSUS-Pinned' -Members ($ring | ForEach-Object { Get-ADComputer $_ }) -Confirm:$false
# Read the WSUS group back. Its membership is now your migration tracker.
Get-ADGroupMember -Identity 'SEC-Srv-WSUS-Pinned' | Select-Object Name
Now the part that catches people. Security filtering is evaluated against the machine’s access token, and a computer’s group memberships travel in a Kerberos ticket it already holds. Changing the group in the directory does not change the ticket, so until it is renewed the machine goes on applying the WSUS policy however often you refresh, and you conclude the filtering is broken when it is working as designed.
A descope that does not appear to take is usually a machine reading a Kerberos ticket issued before you made the change. Purge the ticket or reboot, then judge.
Step 4. Refresh policy and restart the update service
On each server, in an elevated session: purge the computer account’s tickets, refresh computer policy, restart the Windows Update service. The restart is not optional housekeeping. The service reads update policy at scan time and holds what it read, so a scan issued straight after a policy change can answer from the state the service had before it.
klist -li 0x3e7 purge # the computer account's logon session
gpupdate /target:computer /force
Restart-Service wuauserv
# Confirm the WSUS object is no longer applying.
gpresult /scope computer /r
Expected output: gpupdate reports success, and the applied objects list from gpresult shows GPO-Srv-WindowsUpdate-Direct and no longer the WSUS object. If the WSUS object still appears, either the ticket purge did not take, in which case reboot, which always works, or the object reaches the machine by a route other than the security group, which is a finding to resolve before going further.
Step 5. Verify the pin is gone at the registry
Run the same capture from step 1 again, saving it as the post file, and read it against what you saved before. This is the gate in front of the scan, because a scan against a machine that still holds the pin proves nothing at all.
# Same block as step 1, written to the post file.
.\capture-wu-policy.ps1 > ".\cutover-Ring1-20260812\$env:COMPUTERNAME-post.txt"
Get-Content ".\cutover-Ring1-20260812\$env:COMPUTERNAME-post.txt"
Expected output: the parent key either absent or holding no values, and the AU subkey holding exactly one value, NoAutoUpdate = 1, placed there by the successor policy. Nothing else. In particular no WUServer, no WUStatusServer, no UseWUServer, and no DoNotConnectToWindowsUpdateInternetLocations.
If WUServer is still there, work the causes in order. The machine is still in the WSUS group, or its ticket was not purged, which step 4 covers. A second object also sets the value, which the enumeration in the naming section would have found. The value was delivered as a Group Policy preference rather than an administrative template, in which case it tattooed, and you delete it by hand and then go and find the preference item. Or something on the machine is writing it back, classically an endpoint agent with its own update distribution role, in which case it reappears after deletion and the answer is not in Group Policy at all.
Step 6. The dual scan guard
This step exists because the failure it prevents is silent, and because five machines here are exposed: both domain controllers, the legacy application server, the SQL server and WSUS01 itself, all Windows Server 2016 or 2019. Those builds still carry dual scan in 2026, and 2019 never received the scan source settings that replaced it on 2022 and 2025. A machine on those builds carrying both the WSUS pin and any Windows Update for Business deferral setting scans Microsoft for Windows content and ignores WSUS for it, while still reporting to WSUS as though nothing had changed. The guard covers the whole estate rather than the ring you are moving, because the condition fires on machines not yet cut over.
# On any machine NOT yet cut over: the policy key should contain the WSUS pin and
# nothing that looks like a deferral or a scan source setting.
(Get-Item 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate').Property
# Across the domain: does any object carry a Windows Update for Business setting?
Get-GPO -All | ForEach-Object {
$xml = Get-GPOReport -Guid $_.Id -ReportType Xml
if ($xml -match 'Defer|BranchReadiness|SetPolicyDrivenUpdateSource') {
$_ | Select-Object DisplayName, Id
}
}
# Expected: no rows. A row is the finding, and you remove the setting from that object.
If you find one, take the setting out of the offending object. Do not reach for the legacy kill switch as a shortcut: it is filed under legacy configurations, has no effect on the Windows 11 lineage, and configuring it alongside the newer scan source settings on the Windows 10 lineage blocks Windows Update entirely. The rule that keeps this estate safe costs nothing. No Windows Update for Business deferral policies on these servers, ever, because timing lives in the maintenance configuration.
Step 7. Opt in to Microsoft Update where the machine needs it
WSUS was not only offering Windows content. If your synchronisation included the SQL Server or Office product families, those updates reached machines through WSUS and do not arrive by default once a machine goes direct, because Windows Update and Microsoft Update are two different services and the machine is registered with one of them. SQL01 is the case that breaks silently: it goes on patching Windows perfectly, reports clean in Update Manager, and quietly stops receiving SQL Server engine updates. Nothing fails. A column simply stops filling.
Because step 2 set Configure Automatic Updates to Disabled, the checkbox route is unavailable and the opt in is a service registration. It is a per machine change, so decide explicitly for each server rather than applying it everywhere by reflex.
# Register Microsoft Update as an update service on this machine.
$serviceId = '7971f918-a847-4430-9279-4a52d1efe18d'
(New-Object -ComObject 'Microsoft.Update.ServiceManager').AddService2($serviceId, 7, '')
# Verify, two ways.
(New-Object -ComObject 'Microsoft.Update.ServiceManager').Services |
Select-Object Name, IsDefaultAUService, ServiceID
(Get-Item 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Services').GetSubKeyNames()
Expected output: the service list includes an entry for Microsoft Update alongside Windows Update, and the registry enumeration includes the identifier you just registered. The failure mode is not an error, it is the absence of the entry, so read the output rather than trusting the call. If the machine is still pinned at this point the registration can appear to succeed and have no practical effect, which is another reason step 5 comes first.
Step 8. Force a scan and read where it went
On Windows Server 2016 and later the modern force scan is UsoClient.exe StartScan. It returns immediately and prints nothing, so treat silence as normal and give it several minutes. Historic WSUS references in the log are normal, so check timestamps rather than the presence of a string.
UsoClient.exe StartScan
Start-Sleep -Seconds 300
# Decode the event trace into a readable log. Symbol resolution needs internet access;
# without it the cmdlet warns and still produces a usable file.
Get-WindowsUpdateLog -LogPath 'C:\Temp\WindowsUpdate.log'
# Read the WSUS host out of the pre-cutover capture rather than typing it.
$oldWsus = 'wsus01'
Select-String -Path 'C:\Temp\WindowsUpdate.log' -Pattern 'sls.update.microsoft.com', $oldWsus |
Select-Object -First 20
Expected result: matches on the Microsoft service endpoint and no fresh matches on the old WSUS host.
Step 9. Run a full cycle and read the record
This is the end to end test, not a smoke test. Trigger an on demand assessment on the ring from Update Manager so the assessment data reflects the new source, then let the ring’s maintenance configuration fire on its own schedule. Only when that run completes is the cutover proven, because everything before it tested the machine and this tests the whole chain: policy, source, agent, orchestrator and record.
Rerun the step 1 query afterwards and compare it against the exported baseline, machine by machine.
On the source field I am going to be honest rather than tidy. The documented enumeration for patchServiceUsed is Unknown, WU, WU_WSUS, YUM, APT and Zypper, with no bare WSUS value in it, while the prose describing the Resource Graph log structure treats the Windows value as covering both the Windows Update service and Windows Server Update Services. Those are different claims, and I have found no sentence that settles whether a machine sourcing directly from Microsoft reliably reports a value distinct from one sourcing through WSUS.
Where the documentation contradicts itself, measure your own estate. A baseline you captured beats an expected value somebody printed.
So this sheet prints no expected value for that column. What you do instead is the reason step 1 exported a baseline. You hold the same field, for the same machines, from a run you know was WSUS sourced. Compare them. If the value changes you have established the mapping for your own tenant and can use it as a compliance signal for every later ring. If it does not, you have learned that the field is not a source of truth on your estate, and that the registry check in step 5 and the endpoint check in step 8 are the evidence your change record should carry.
Worked example: AZSRV02, the Azure machine that inherits the pin
AZSRV02 is a Windows Server 2022 virtual machine in Azure, domain joined to catsnackjack.com across the site to site link, tagged PatchRing = Ring1. It is the most interesting machine in the estate here, for three reasons.
It receives the WSUS Group Policy like any other domain member, which means an Azure virtual machine has been reaching back across a VPN tunnel to a server in a comms room to ask what updates it may install. Second, it is an Azure virtual machine rather than an Arc machine, so Update Manager does write to its update registry when it is Azure orchestrated, setting automatic update behaviour, reboot state and a service identifier. Expect values under the non policy Windows Update key that you did not put there and should not remove. Third, it runs Windows Server 2022, so it sits on the scan source side of the dual scan divide, which makes it a good first machine and a poor rehearsal for the domain controllers.
| Decision for AZSRV02 | Value | Why |
|---|---|---|
| Ring | Ring1 | General ring. It moves with FS01 and APP02, after the pilot proves the procedure. |
| Microsoft Update opt in, step 7 | Not applied | No SQL Server or Office family workload. Recorded rather than skipped, so nobody rediscovers it. |
| Dual scan guard, step 6 | Checked, not applicable | Windows Server 2022 uses scan source rather than dual scan. The estate rule against deferral policies still applies. |
| Patch orchestration | Customer Managed Schedules | Required on Azure virtual machines for scheduled patching to run at all. Unchanged here, but confirmed rather than assumed. |
| Restart | Whatever the Ring1 maintenance configuration states | The successor GPO configures no restart behaviour, so one authority owns reboots and it is the schedule. |
The sequence, end to end, then leave it alone until the Ring1 window fires and read the record. Run the step 1 query filtered to Ring1, confirm AZSRV02 has a Succeeded installation record inside the retention window, and export it. Capture its policy key contents, which is where the real WSUS URL is recorded. Add its computer object to SEC-Srv-WU-Direct and remove it from SEC-Srv-WSUS-Pinned. On the machine, purge the computer logon session’s tickets, force a computer policy refresh, restart wuauserv, and confirm with gpresult that the WSUS object no longer applies. Re-run the registry capture and confirm the parent policy key holds no values and the AU subkey holds only NoAutoUpdate = 1. Skip step 7 deliberately. Force a scan with UsoClient.exe StartScan, decode the log, and confirm the fresh entries reach the Microsoft endpoint rather than the WSUS host.
One consequence to tell your network team before rather than after. Once the pin goes, AZSRV02 stops pulling content across the tunnel and starts pulling it from Microsoft directly. That is a straight improvement to the link and a change in the traffic profile at the gateway. A machine that never needed outbound access to Microsoft’s update endpoints now needs it, so if your network security group was written on the assumption that this server only talks to the corporate network, the cutover fails here, and it fails as a machine that finds no updates rather than as an error.
Rollback
Rollback is the reason the technique is descope rather than edit. It is one operation reversed.
$back = 'AZSRV02'
Add-ADGroupMember -Identity 'SEC-Srv-WSUS-Pinned' -Members (Get-ADComputer $back)
Remove-ADGroupMember -Identity 'SEC-Srv-WU-Direct' -Members (Get-ADComputer $back) -Confirm:$false
# On the machine
klist -li 0x3e7 purge
gpupdate /target:computer /force
Restart-Service wuauserv
Get-ItemPropertyValue -Path 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate' -Name WUServer
Expected on the next refresh: WUServer and WUStatusServer return, UseWUServer returns as 1, and the value matches the string in that machine’s pre-cutover capture file. Compare against the file, not your memory, because a value that comes back slightly different is a different finding entirely.
Two things rollback does not do, and both should be said aloud to whoever authorised the change. It does not uninstall anything the machine already took from Microsoft; those updates stay, and WSUS reports them as installed or not applicable once the machine next reports in, on the client’s own reporting cycle rather than on your refresh. And it does not undo the service registration from step 7, so a machine rolled back after that step is pinned to WSUS and also registered with Microsoft Update. Not harmful, but a state nobody intended, and it belongs in the change record.
Where this goes next
Repeat from step 1 for the next ring, with the same gate in front. The sensitive ring, holding the domain controllers, the SQL server and the legacy application server, is where step 6 stops being a formality and step 7 stops being optional, so do not let a smooth first ring persuade anyone to compress the procedure.
When SEC-Srv-WSUS-Pinned is empty, the source cutover is finished and the box has nothing left talking to it. The next sheet takes it out of service: stragglers found by last contact time before anything is destroyed, the approval history and the content inventory taken off the machine while it still exists, and only then the role, the database, the site and the disk.
Azure Update Manager
‹ Previous: [AUM 5] The Cutover: Leaving WSUS Without Breaking the Estate
Next: [AUM 5.2] Build Sheet: Decommissioning WSUS01 ›




