~/2025/04/03/powershell-windows-disable-ipv6-fleet-wide-via-group-policy-startup-script.md
PowerShell: Windows – Disable IPv6 Fleet-Wide via GPO Startup Script
--- author: Tom Lasswell date: read: 5 min in: [ps, scripts] tags: [powershell, windows, ipv6, gpo] ---
$ grep -n '^#' post.md
Every so often a vendor tells me IPv6 is the reason their agent won't check in, or a monitoring tool logs a flood of link-local addresses nobody asked for, and someone asks for IPv6 to be turned off across the fleet. Before you do that, read what Microsoft actually says in KB 929852 ("Configure IPv6 for advanced users"): IPv6 is a mandatory part of Windows since Vista and Server 2008, Microsoft does not recommend disabling IPv6, disabling its components, or unbinding it from interfaces ("some Windows components might not function"), and it recommends the Prefer IPv4 over IPv6 prefix policy instead. The knob for either option is the DisabledComponents DWORD under HKLM\SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters. This script writes it from a Group Policy computer startup script, defaults to the recommended 0x20 (prefer IPv4), only goes to the full 0xFF disable when you ask for it, logs what it changed, and warns about adapters where someone unbound IPv6 by hand. The longer story of what breaks when you go further is in Windows Server: Disabling IPv6 Company-Wide — What Actually Broke.
Requirements
- Windows PowerShell 5.1 (built into Windows 10 / Server 2016 and later). No external modules;
Get-NetAdapterBindingis in the built-inNetAdaptermodule. - Rights to create and link a GPO in the target OU (delegated GPO rights or Domain Admin). The
GroupPolicymodule (RSAT) if you script the GPO side. - The script must run under Computer Configuration (startup script), not User Configuration: it writes an
HKLMvalue, and computer startup scripts run as SYSTEM before anyone logs on. - A restart after the value changes. Microsoft's note on the key: "You must restart your computer for these changes to take effect."
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
-DisabledComponentsValue | int | No | DWORD written to DisabledComponents. Default 32 (0x20, prefer IPv4 over IPv6, Microsoft's recommended setting). Allowed: 0 (default Windows behaviour, used for rollback), 1 (0x01, disable tunnel interfaces), 32 (0x20), 33 (0x21, prefer IPv4 and disable tunnels), 255 (0xFF, disable IPv6 on everything except loopback). |
-LogPath | string | No | Folder for the per-day log file. Default C:\ProgramData\GPStartupLogs. |
The values come straight from the KB's bitmask table: bit 0 disables tunnel interfaces, bit 4 disables native (non-tunnel) interfaces, bit 5 sets the prefer-IPv4 prefix policy, and 0xFF sets every bit. 0x21 is simply bit 0 plus bit 5.
Usage
Test on a single machine first, from an elevated session, and look at the log and the resulting value:
.\Set-Ipv6DisabledComponents.ps1 -Verbose
Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters" -Name DisabledComponents
Get-Content -Path "C:\ProgramData\GPStartupLogs\Set-Ipv6DisabledComponents-$(Get-Date -Format 'yyyyMMdd').log"
2025-04-03 07:12:41 Startup script running on SRV-APP01. Target DisabledComponents = 0x20.
2025-04-03 07:12:41 DisabledComponents changed from <not set> to 0x20. Restart required.
After the restart, confirm the prefix policy changed. The KB's check: in the output, the ::ffff:0:0/96 (IPv4-mapped) prefix must now have a higher precedence number than ::/0, and ping <hostname> for a dual-stack name should answer from its IPv4 address:
netsh interface ipv6 show prefixpolicies
Only if you have a documented reason to go further, fully disable IPv6 (everything but loopback) on a pilot box:
.\Set-Ipv6DisabledComponents.ps1 -DisabledComponentsValue 255
Roll back by writing the default value 0:
.\Set-Ipv6DisabledComponents.ps1 -DisabledComponentsValue 0
Create and link the GPO once the script is in SYSVOL, then attach it under Computer Configuration > Policies > Windows Settings > Scripts (Startup/Shutdown) > Startup, on the PowerShell Scripts tab, in the Group Policy Management Editor. The GroupPolicy module creates and links the GPO, but it has no cmdlet for the startup-script assignment:
New-GPO -Name "IPv6 - Prefer IPv4" | New-GPLink -Target "OU=Servers,DC=<domain>,DC=<tld>"
If you don't need the logging or the binding check, a registry-based policy does the same job without any script, and it stays enforced on every Group Policy refresh:
Set-GPRegistryValue -Name "IPv6 - Prefer IPv4" -Key "HKLM\SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters" -ValueName "DisabledComponents" -Type DWord -Value 32
Script
<#
.SYNOPSIS
Sets the Tcpip6 DisabledComponents value from a Group Policy startup script.
.DESCRIPTION
Writes the DisabledComponents DWORD under
HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters. The default, 0x20,
is Microsoft's recommended "Prefer IPv4 over IPv6" prefix policy, which keeps
the IPv6 stack running. 0xFF disables IPv6 on all interfaces except loopback,
which Microsoft does not recommend. The script only writes the registry when
the current value differs, warns about adapters where IPv6 has been unbound
(an unsupported configuration), and logs every run to a local file. It runs
on every boot, so machines that were offline at rollout pick it up later.
.PARAMETER DisabledComponentsValue
DWORD to write. 0 (default Windows behaviour, rollback), 1 (0x01, disable
tunnel interfaces), 32 (0x20, prefer IPv4), 33 (0x21, prefer IPv4 and
disable tunnels) or 255 (0xFF, disable IPv6 except loopback). Default 32.
.PARAMETER LogPath
Folder for the per-day log file. Default C:\ProgramData\GPStartupLogs.
.EXAMPLE
.\Set-Ipv6DisabledComponents.ps1
.EXAMPLE
.\Set-Ipv6DisabledComponents.ps1 -DisabledComponentsValue 255 -LogPath "C:\Logs\Ipv6"
.NOTES
Author : Thomas Lasswell (https://www.techcolumnist.com)
Version : 1.0 (2025-04-03)
Requires: Windows PowerShell 5.1, NetAdapter module, run as SYSTEM or Administrator
.LINK
https://learn.microsoft.com/en-us/troubleshoot/windows-server/networking/configure-ipv6-in-windows
#>
[CmdletBinding()]
param (
[Parameter()]
[ValidateSet(0, 1, 32, 33, 255)]
[int]$DisabledComponentsValue = 32,
[Parameter()]
[string]$LogPath = "C:\ProgramData\GPStartupLogs"
)
$regPath = "HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters"
$regName = "DisabledComponents"
$targetHex = "0x{0:X2}" -f $DisabledComponentsValue
if (-not (Test-Path -Path $LogPath)) {
New-Item -Path $LogPath -ItemType Directory -Force | Out-Null
}
$logFile = Join-Path -Path $LogPath -ChildPath "Set-Ipv6DisabledComponents-$(Get-Date -Format 'yyyyMMdd').log"
function Write-Log {
param (
[string]$Message
)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
"$timestamp $Message" | Out-File -FilePath $logFile -Append -Encoding utf8
Write-Verbose -Message $Message
}
Write-Log "Startup script running on $env:COMPUTERNAME. Target DisabledComponents = $targetHex."
# Unbinding IPv6 from an adapter is a separate, unsupported change. Report it, don't touch it.
$unbound = Get-NetAdapterBinding -ComponentID ms_tcpip6 -ErrorAction SilentlyContinue | Where-Object { -not $_.Enabled }
foreach ($binding in $unbound) {
Write-Log "WARNING: IPv6 (ms_tcpip6) is unbound on adapter '$($binding.Name)'. Microsoft does not support this; re-enable with Enable-NetAdapterBinding."
}
# Read the current value. A missing value means the Windows default (0).
$currentValue = $null
$property = Get-ItemProperty -Path $regPath -Name $regName -ErrorAction SilentlyContinue
if ($property) {
# A REG_DWORD comes back as a signed Int32, so 0xFFFFFFFF reads as -1.
$currentValue = [int64]$property.$regName
if ($currentValue -lt 0) {
$currentValue += 4294967296
}
}
if ($currentValue -eq 4294967295) {
Write-Log "DisabledComponents is 0xFFFFFFFF, which Microsoft documents as incorrect (it adds a 5-second startup delay). Correcting it."
}
$currentHex = if ($null -eq $currentValue) { "<not set>" } else { "0x{0:X2}" -f $currentValue }
if ($currentValue -eq $DisabledComponentsValue) {
Write-Log "DisabledComponents already $targetHex. No change made."
exit 0
}
if ($null -eq $currentValue -and $DisabledComponentsValue -eq 0) {
Write-Log "DisabledComponents not set, which is already the default. No change made."
exit 0
}
if ($DisabledComponentsValue -notin @(0, 32)) {
Write-Log "WARNING: $targetHex is outside 0 and 0x20. Microsoft documents that such values make the Routing and Remote Access service fail."
}
try {
New-ItemProperty -Path $regPath -Name $regName -Value $DisabledComponentsValue -PropertyType DWord -Force -ErrorAction Stop | Out-Null
Write-Log "DisabledComponents changed from $currentHex to $targetHex. Restart required."
exit 3010
} catch {
Write-Log "Failed to set DisabledComponents: $($_.Exception.Message)"
exit 1
}
Notes
- Microsoft's position, in its own words: "We don't recommend that you disable IPv6 or IPv6 components or unbind IPv6 from interfaces" and "We recommend using Prefer IPv4 over IPv6 in prefix policies instead of disabling IPv6." That is why the default here is
0x20.0xFFis supported as a registry setting, but when you open a case Microsoft may ask you to re-enable IPv6 before troubleshooting. 0xFF, not0xFFFFFFFF. The KB states the correct full-disable value is0xFF; setting0xffffffffdelays startup by five seconds. The script detects and overwrites the wrong value.- RRAS. The KB warns that values other than
0or32make the Routing and Remote Access service fail. Keep RRAS servers (including DirectAccess and VPN servers built on it) out of any GPO that sets0x01,0x21or0xFF, for example with a security-group or WMI filter. - Loopback survives. Even at
0xFFyou "cannot completely disable IPv6 as IPv6 is used internally on the system for many TCPIP tasks";ping ::1still answers. That is by design. - The checkbox lies.
DisabledComponentsdoesn't change the "Internet Protocol Version 6 (TCP/IPv6)" checkbox on the adapter; the box can stay ticked while IPv6 is disabled. Check the registry value andnetsh interface ipv6 show prefixpolicies, not the GUI. Conversely, unticking the box (orDisable-NetAdapterBinding -ComponentID ms_tcpip6) is the unbinding Microsoft calls potentially "an unsupported Windows configuration", which is why the script only reports it. - Tunnels only. If the actual complaint is stray 6to4 addresses (Windows enables 6to4 when an interface has a public IPv4 address and registers those addresses in DNS),
0x01or the Group Policy settings under Computer Configuration > Administrative Templates > Network > TCPIP Settings > IPv6 Transition Technologies (6to4, ISATAP and Teredo State = Disabled) solve it without touching native IPv6. ISATAP and Teredo are already disabled by default. - Microsoft's KB lists known casualties of disabling IPv6: LDAP over UDP 389 on domain controllers, Exchange Server, and failover clusters. Pilot on a small OU and keep domain controllers, Exchange, cluster nodes and RRAS servers out of the first waves.
- Exit code
3010is the conventional "success, restart required" code from Windows Installer. Group Policy ignores it, but it is useful if an RMM or ConfigMgr run of the same script reads the exit code.