~/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: 
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-NetAdapterBinding is in the built-in NetAdapter module.
  • Rights to create and link a GPO in the target OU (delegated GPO rights or Domain Admin). The GroupPolicy module (RSAT) if you script the GPO side.
  • The script must run under Computer Configuration (startup script), not User Configuration: it writes an HKLM value, 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

NameTypeRequiredDescription
-DisabledComponentsValueintNoDWORD 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).
-LogPathstringNoFolder 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:

powershell
.\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"
text
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:

powershell
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:

powershell
.\Set-Ipv6DisabledComponents.ps1 -DisabledComponentsValue 255

Roll back by writing the default value 0:

powershell
.\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:

powershell
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:

powershell
Set-GPRegistryValue -Name "IPv6 - Prefer IPv4" -Key "HKLM\SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters" -ValueName "DisabledComponents" -Type DWord -Value 32

Script

powershell
<#
.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. 0xFF is supported as a registry setting, but when you open a case Microsoft may ask you to re-enable IPv6 before troubleshooting.
  • 0xFF, not 0xFFFFFFFF. The KB states the correct full-disable value is 0xFF; setting 0xffffffff delays startup by five seconds. The script detects and overwrites the wrong value.
  • RRAS. The KB warns that values other than 0 or 32 make the Routing and Remote Access service fail. Keep RRAS servers (including DirectAccess and VPN servers built on it) out of any GPO that sets 0x01, 0x21 or 0xFF, for example with a security-group or WMI filter.
  • Loopback survives. Even at 0xFF you "cannot completely disable IPv6 as IPv6 is used internally on the system for many TCPIP tasks"; ping ::1 still answers. That is by design.
  • The checkbox lies. DisabledComponents doesn'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 and netsh interface ipv6 show prefixpolicies, not the GUI. Conversely, unticking the box (or Disable-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), 0x01 or 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 3010 is 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.

Source