Configuring Proper Time Synchronization in an Active Directory Domain

Wall clock representing time synchronization

Accurate time is critical in an Active Directory environment. Kerberos authentication depends on it, and by default it tolerates no more than five minutes of clock skew between a client and a domain controller. Certificate validity periods, CRL publication, OCSP responses, and log correlation all depend on it too, which becomes even more important as organizations move to short-lived certificates. During a recent engagement to assess and remediate a customer’s Active Directory Certificate Services (AD CS) deployment, I noticed intermittent Kerberos authentication anomalies that didn’t line up with anything in the PKI itself. The root cause turned out to be time synchronization, which nobody had configured correctly across the domain. This post explains what I found and how I fixed it. I’ve reproduced the customer’s configuration in my lab, so the examples and output come from lab.richardhicks.net, but the process is the same one I used in the customer’s environment.

What I Found

When I reviewed time configuration across the customer’s domain, I found several problems, including the following.

  • Inconsistent domain controller configuration. Some domain controllers had never been configured and were still running the defaults. Others had been pointed manually at different external time sources, presumably by different administrators at different times. This resulted in more than one time root in the domain, which is exactly what the Windows time hierarchy is designed to prevent.
  • The PDC emulator was not authoritative. The domain controller holding the PDC emulator role had no external time source configured, so it wasn’t anchoring the domain to UTC.
  • Hyper-V was quietly providing the time. Most of the domain controllers were virtual machines running on Hyper-V. The Hyper-V time synchronization integration service (VMICTimeProvider) was enabled on all of them, and W32Time on the domain controllers was taking time from the host instead of the domain hierarchy.
  • The Hyper-V hosts had no time configuration at all. Most of the domain was running on Hyper-V hosts that were workgroup members. Workgroup machines don’t participate in the domain time hierarchy, and nobody had configured them. They were running on their own clocks, polling time.windows.com infrequently, if they could reach it at all.

The result was a loop with no anchor. The domain controllers took their time from the hosts, the hosts drifted on their own, and nothing in the environment was reliably tied to UTC. Different domain controllers on different hosts could disagree by a meaningful amount, which explained the inconsistent Kerberos behavior. In my lab, the PDC emulator showed the problem clearly.

PS C:\> w32tm.exe /query /status
Leap Indicator: 0(no warning)
Stratum: 1 (primary reference - syncd by radio clock)
...
ReferenceId: 0x564D5450 (source name: "VMTP")
...
Source: VM IC Time Synchronization Provider

Don’t be fooled by Stratum 1. The VM IC provider always reports Stratum 1, but the real source is the Hyper-V host’s clock. A quick comparison against a public time server showed the PDC emulator was about 840 milliseconds off UTC.

PS C:\> w32tm.exe /stripchart /computer:time.cloudflare.com /samples:3 /dataonly
Tracking time.cloudflare.com [162.159.200.1:123].
Collecting 3 samples.
17:36:52, +00.8405031s
17:36:54, +00.8443389s
17:36:56, +00.8415804s

That’s not enough to break Kerberos on its own, but it was drifting, and in the customer’s environment, with multiple hosts and multiple time roots, the differences were much larger.

How Time Sync Should Work

Before making changes, it’s worth reviewing how Windows time synchronization is designed to work in an Active Directory domain. The hierarchy is simple and consists of the following.

  • The forest root PDC emulator is the authoritative time source. It is the only domain controller that should synchronize with an external source, and it advertises itself as a reliable time server (AnnounceFlags 5).
  • All other domain controllers follow the domain hierarchy. They use the NT5DS type and synchronize with the PDC emulator (directly or through their domain’s PDC emulator in a multi-domain forest). They advertise as time servers, but not as reliable ones (AnnounceFlags 10, the default).
  • Domain members synchronize with a domain controller. This is the default behavior for every domain-joined machine, although virtual machines need one adjustment, which I cover next.

Virtual Machines

Virtualization adds a wrinkle. On Hyper-V, the time synchronization integration service registers its own W32Time provider, VMICTimeProvider, which feeds the guest time from the host. It’s enabled by default on every virtual machine, and on a domain-joined VM it competes with the domain hierarchy and usually wins. Microsoft’s guidance is to disable VMICTimeProvider on virtual domain controllers so they get their time from the PDC emulator instead. I recommend disabling it on domain member VMs as well, so every domain-joined machine follows the same hierarchy.

Workgroup Hyper-V Hosts

The Hyper-V hosts matter too. If they’re domain members, they’ll follow the domain hierarchy automatically. If they’re workgroup members, as they were here, they need to be configured explicitly. Even with VMICTimeProvider disabled, the host’s clock still sets each VM’s time at startup and after a restore from a saved state, and the host needs accurate time for its own logs and certificates.

Target Configuration

The end state looks like this. The PDC emulator and the Hyper-V hosts both sync with the same public source, and every domain-joined machine, physical or virtual, follows the domain hierarchy.

SystemTime sourceConfigured by
PDC emulatortime.cloudflare.comDC Time Sync GPO
Other domain controllersPDC emulatorDC Time Settings GPO
Domain members (physical and virtual)Domain controllersDomain Member Time Settings GPO
Hyper-V hosts (workgroup)time.cloudflare.comLocal configuration

Resetting Domain Controllers to the Default Configuration

With domain controllers configured inconsistently, the cleanest approach is to return every one of them to the default configuration and then manage time centrally with Group Policy. Resetting first is important. Local settings remain in the registry underneath any policy, and if a domain controller ever stops receiving the policy (for example, after the PDC emulator role moves), it falls back to whatever was configured locally. Leftover manual peer settings would quietly create a second time root.

Restoring the Defaults

The following commands reset W32Time on every domain controller in the domain. They re-enable the Hyper-V time provider (the default), restore domain hierarchy synchronization, reset the peer list to the stock time.windows.com,0x9 value, and set AnnounceFlags back to 10 with /reliable:no.

$DomainControllers = (Get-ADDomainController -Filter *).HostName
Invoke-Command -ComputerName $DomainControllers -ScriptBlock {
# Re-enable the Hyper-V time provider (default)
$VmicKey = 'HKLM:\SYSTEM\CurrentControlSet\Services\W32Time\TimeProviders\VMICTimeProvider'
If (Test-Path -Path $VmicKey) {
Set-ItemProperty -Path $VmicKey -Name Enabled -Value 1
}
# Restore domain hierarchy sync, default peer list, and default AnnounceFlags (10)
w32tm.exe /config /manualpeerlist:"time.windows.com,0x9" /syncfromflags:domhier /reliable:no /update
Restart-Service -Name W32Time
w32tm.exe /resync /rediscover
}

I recommend this targeted approach over w32tm.exe /unregister and w32tm.exe /register. Re-registering the service rebuilds the entire W32Time configuration, but it can also remove the VMICTimeProvider subkey. That key is created by the Hyper-V integration components, not by W32Time, and it’s the key we’ll target with Group Policy in the next step.

Verifying the Reset

Next, confirm each domain controller is running the defaults and that no existing Group Policy is configuring W32Time.

Invoke-Command -ComputerName $DomainControllers -ScriptBlock {
Write-Output "=== $env:COMPUTERNAME ==="
w32tm.exe /query /configuration | Select-String 'Type:|NtpServer:|AnnounceFlags:' | ForEach-Object { $_.Line.Trim() }
w32tm.exe /query /source
Write-Output "Time policy present: $(Test-Path -Path 'HKLM:\SOFTWARE\Policies\Microsoft\W32Time')"
}

Each domain controller should report Type: NT5DS (Local) and AnnounceFlags: 10 (Local), and the time policy check should return False. If it returns True, another GPO is already configuring W32Time. Find it and remove those settings before continuing. Don’t be surprised if the source still shows VM IC Time Synchronization Provider on some domain controllers at this point. That’s the default behavior we’re about to fix.

Configuring Time Sync with Group Policy

With every domain controller back to a known state, the next step is to configure time centrally with three GPOs. Two are linked to the Domain Controllers OU, and the third is linked to the domain root for domain members. The design includes the following.

GPOApplies toSettings
DC Time SettingsAll domain controllersType NT5DS, AnnounceFlags 10, NTP client and server enabled, VMICTimeProvider disabled
DC Time SyncPDC emulator only (WMI filter)Type NTP, NtpServer time.cloudflare.com,0x8 time.nist.gov,0xa, AnnounceFlags 5
Domain Member Time SettingsAll domain members (linked to the domain root)VMICTimeProvider disabled

DC Time Sync is linked with a higher precedence (link order 1), so on the PDC emulator its values override the baseline from DC Time Settings. Every other domain controller receives only the baseline. A nice benefit of this design is that it follows the PDC emulator role automatically. If the role moves to another domain controller, the WMI filter matches the new role holder, and the old one falls back to the baseline with no administrator intervention. However, if the PDC emulator role moves to another DC, don’t forget to update the outbound access rules on the edge firewall to allow the new server to access the public time resource.

Choosing a Time Source

I used time.cloudflare.com as the primary time source for this customer, with time.nist.gov as a fallback. You can use any reliable public time service, such as time.windows.com or a regional pool.ntp.org address. Separate multiple servers with spaces in the NtpServer value.

Adding a Fallback Time Source

A single upstream time source works, but it makes the entire domain dependent on one provider. Microsoft recommends configuring three or more time servers when using multiple peers, or, with only two, marking one with the UseAsFallbackOnly flag (0x2). I chose the latter, setting NtpServer to time.cloudflare.com,0x8 time.nist.gov,0xa. The 0xa flag combines client mode (0x8) and fallback only (0x2), so W32Time uses NIST only when Cloudflare is unavailable. Keep the following in mind when choosing a fallback.

  • It removes a single point of failure. Cloudflare’s time service is highly reliable, but the service itself isn’t the only thing that can fail. A DNS resolution failure, a BGP route leak or hijack affecting the provider’s prefixes, an upstream ISP routing problem, or a firewall change can all make a single provider unreachable from your network. NIST runs on a completely separate network and doesn’t use anycast, so an event that affects one provider is unlikely to affect the other.
  • An outage isn’t catastrophic. Even if the PDC emulator can’t reach any upstream source, it keeps running on its own clock and continues serving time to the rest of the domain. Every domain controller and domain member still follows the PDC emulator, so the domain stays in sync with itself while slowly drifting from UTC. That’s what matters most for Kerberos, which compares the client’s clock with the domain controller’s clock, not with UTC. A typical system clock drifts by a few seconds per day at most, so an outage lasting hours or even days has no practical impact. When the time source becomes reachable again, W32Time corrects the accumulated offset automatically.
  • Both sources must handle leap seconds the same way. Cloudflare and NIST both insert leap seconds as a one-second step, so they’re a safe pair. Google, AWS, and Meta smear leap seconds instead, so don’t pair one of them with Cloudflare or NIST. The next section explains why.
  • A fallback doesn’t improve accuracy. A second source improves availability, not accuracy. With UseAsFallbackOnly, W32Time only consults NIST when Cloudflare is unreachable, so there’s nothing to cross-check against. Even with two active peers, there’s no majority to decide which one is right if they disagree. That’s fine for a typical Active Directory domain, which doesn’t require high-accuracy time.

Leap Second Considerations

Avoid mixing time services that handle leap seconds differently. A leap second is an occasional one-second adjustment to UTC that keeps it aligned with the Earth’s rotation. Most time services, including Cloudflare and NIST, apply it the standard way, by inserting the extra second all at once. Google (and AWS) instead “smear” it by slightly slowing their clocks over a 24-hour window, so during that period their time differs from other services by up to half a second. If a domain controller has both kinds of servers in its peer list, they disagree during a leap event, and the results are unpredictable. Pick one approach and use it consistently. You can read more about each approach in Google’s leap smear documentation and Cloudflare’s NTP documentation.

Allowing Outbound NTP

The PDC emulator must be able to reach both time services on UDP port 123. In the customer’s environment, as in my lab, outbound NTP was blocked at the edge firewall. I added a rule allowing outbound UDP 123 from the PDC emulator to the IPv4 and IPv6 addresses of both time services. Cloudflare publishes its addresses in its documentation, and NIST publishes the list of servers that time.nist.gov rotates across. Be sure to include both address families if your domain controllers have IPv6 connectivity, because W32Time will often prefer IPv6 when the name resolves to both. Also, remember to update the rule if the PDC emulator role moves to a different server. You can validate reachability to both services from the PDC emulator before configuring anything.

w32tm.exe /stripchart /computer:time.cloudflare.com /samples:3 /dataonly
w32tm.exe /stripchart /computer:time.nist.gov /samples:3 /dataonly

If both return offsets, you’re good to go. An error of 0x800705B4 indicates a timeout, which usually means UDP 123 is blocked or the IPv6 path isn’t working.

Creating the GPOs

The following script creates the WMI filter and all three GPOs. It links the two domain controller GPOs to the Domain Controllers OU and the member GPO to the domain root. It requires the GroupPolicy and ActiveDirectory modules (Install-WindowsFeature -Name GPMC, RSAT-AD-PowerShell on Windows Server) and Domain Admin rights. Run it in Windows PowerShell 5.1. You can also download the script from GitHub.

#Requires -Modules GroupPolicy, ActiveDirectory
$ErrorActionPreference = 'Stop'
# Variables
$Domain = Get-ADDomain
$DomainFqdn = $Domain.DNSRoot
$DomainDn = $Domain.DistinguishedName
$DcOu = $Domain.DomainControllersContainer
$Pdc = $Domain.PDCEmulator
$PolicyKey = 'HKLM\SOFTWARE\Policies\Microsoft\W32Time'
$NtpServer = 'time.cloudflare.com,0x8 time.nist.gov,0xa'
# Create the WMI filter (or reuse it if it already exists)
$WmiFilterName = 'PDC Emulator'
$WmiQuery = 'SELECT * FROM Win32_ComputerSystem WHERE DomainRole = 5'
$SomPath = "CN=SOM,CN=WMIPolicy,CN=System,$DomainDn"
$WmiFilter = Get-ADObject -Server $Pdc -SearchBase $SomPath -LDAPFilter "(&(objectClass=msWMI-Som)(msWMI-Name=$WmiFilterName))" -Properties 'msWMI-ID'
If ($null -eq $WmiFilter) {
$WmiId = "{$([Guid]::NewGuid().ToString().ToUpper())}"
$Now = (Get-Date).ToUniversalTime().ToString('yyyyMMddHHmmss.ffffff-000')
$FilterParams = @{
Server = $Pdc
Name = $WmiId
Type = 'msWMI-Som'
Path = $SomPath
OtherAttributes = @{
'msWMI-Name' = $WmiFilterName
'msWMI-Parm1' = 'Domain controller holding the PDC emulator role'
'msWMI-Parm2' = "1;3;10;$($WmiQuery.Length);WQL;root\CIMv2;$WmiQuery;"
'msWMI-Author' = "$env:USERNAME@$DomainFqdn"
'msWMI-ID' = $WmiId
'msWMI-CreationDate' = $Now
'msWMI-ChangeDate' = $Now
}
}
New-ADObject @FilterParams
}
Else {
$WmiId = $WmiFilter.'msWMI-ID'
}
# DC Time Settings (all domain controllers)
$AllDcGpo = New-GPO -Name 'DC Time Settings' -Comment 'Baseline W32Time settings for all domain controllers' -Server $Pdc
$Gpo = @{ Name = $AllDcGpo.DisplayName; Server = $Pdc }
Set-GPRegistryValue @Gpo -Key "$PolicyKey\Parameters" -ValueName 'Type' -Type String -Value 'NT5DS' | Out-Null
Set-GPRegistryValue @Gpo -Key "$PolicyKey\Config" -ValueName 'AnnounceFlags' -Type DWord -Value 10 | Out-Null
Set-GPRegistryValue @Gpo -Key "$PolicyKey\TimeProviders\NtpClient" -ValueName 'Enabled' -Type DWord -Value 1 | Out-Null
Set-GPRegistryValue @Gpo -Key "$PolicyKey\TimeProviders\NtpServer" -ValueName 'Enabled' -Type DWord -Value 1 | Out-Null
# Disable the Hyper-V time provider (Group Policy Preference, writes to the service key)
$PrefParams = @{
Name = $AllDcGpo.DisplayName
Server = $Pdc
Context = 'Computer'
Action = 'Update'
Key = 'HKLM\SYSTEM\CurrentControlSet\Services\W32Time\TimeProviders\VMICTimeProvider'
ValueName = 'Enabled'
Type = 'DWord'
Value = 0
}
Set-GPPrefRegistryValue @PrefParams | Out-Null
# DC Time Sync (PDC emulator only)
$PdcGpo = New-GPO -Name 'DC Time Sync' -Comment 'External NTP source for the PDC emulator' -Server $Pdc
$Gpo = @{ Name = $PdcGpo.DisplayName; Server = $Pdc }
Set-GPRegistryValue @Gpo -Key "$PolicyKey\Parameters" -ValueName 'Type' -Type String -Value 'NTP' | Out-Null
Set-GPRegistryValue @Gpo -Key "$PolicyKey\Parameters" -ValueName 'NtpServer' -Type String -Value $NtpServer | Out-Null
Set-GPRegistryValue @Gpo -Key "$PolicyKey\Config" -ValueName 'AnnounceFlags' -Type DWord -Value 5 | Out-Null
# Assign the WMI filter to DC Time Sync
$PdcGpoDn = "CN={$($PdcGpo.Id.ToString().ToUpper())},CN=Policies,CN=System,$DomainDn"
Set-ADObject -Server $Pdc -Identity $PdcGpoDn -Replace @{ gPCWQLFilter = "[$DomainFqdn;$WmiId;0]" }
# Domain Member Time Settings (all domain members)
$MemberGpo = New-GPO -Name 'Domain Member Time Settings' -Comment 'Disable the Hyper-V time provider so domain members follow the domain hierarchy' -Server $Pdc
$PrefParams = @{
Name = $MemberGpo.DisplayName
Server = $Pdc
Context = 'Computer'
Action = 'Update'
Key = 'HKLM\SYSTEM\CurrentControlSet\Services\W32Time\TimeProviders\VMICTimeProvider'
ValueName = 'Enabled'
Type = 'DWord'
Value = 0
}
Set-GPPrefRegistryValue @PrefParams | Out-Null
# Link both DC GPOs (DC Time Sync takes precedence)
New-GPLink -Name 'DC Time Sync' -Target $DcOu -LinkEnabled Yes -Order 1 -Server $Pdc | Out-Null
New-GPLink -Name 'DC Time Settings' -Target $DcOu -LinkEnabled Yes -Order 2 -Server $Pdc | Out-Null
# Link the member GPO to the domain root
New-GPLink -Name 'Domain Member Time Settings' -Target $DomainDn -LinkEnabled Yes -Server $Pdc | Out-Null
Get-GPInheritance -Target $DcOu -Server $Pdc | Select-Object -ExpandProperty GpoLinks | Format-Table DisplayName, Order, Enabled
Get-GPInheritance -Target $DomainDn -Server $Pdc | Select-Object -ExpandProperty GpoLinks | Format-Table DisplayName, Order, Enabled

There are a few things worth noting about this script.

  • Everything targets the PDC emulator. The GroupPolicy cmdlets talk to the PDC emulator by default, but the ActiveDirectory cmdlets can pick any domain controller. Without -Server, Set-ADObject may land on a domain controller where the new GPO hasn’t replicated yet, and the script fails partway through. I hit this in the field, so the script now pins every call to the PDC emulator.
  • Only the required values are written. Enabling the Global Configuration Settings Administrative Template writes every value it contains, including some outdated clock discipline defaults. Setting individual registry values avoids that, and everything not set by policy keeps its default. The trade-off is that GPMC may show these under Extra Registry Settings instead of Windows Time Service. That’s cosmetic.
  • VMICTimeProvider is disabled with a preference, not a policy. There is no Administrative Template setting for it. The Group Policy Preference writes directly to the service key, so it persists even if the GPO stops applying, which is what you want for a domain controller.
  • The member GPO needs only the preference. Domain members already default to NT5DS, so VMICTimeProvider is the only thing keeping virtual machines off the domain hierarchy. Because it’s linked to the domain root, it also applies to domain controllers. That’s harmless, since DC Time Settings sets the same value and takes precedence.
  • The WMI filter is assigned directly. The GroupPolicy module has no cmdlet for creating or assigning WMI filters, so the script writes the gPCWQLFilter attribute itself.
  • Multi-domain forests. DomainRole = 5 matches the PDC emulator of any domain. Link DC Time Sync only in the forest root domain. Child domain PDC emulators should follow the domain hierarchy.
DC Time Sync and DC Time Settings GPOs linked to the Domain Controllers OU

GPOs applied to domain controllers.

DC Time Sync GPO with the PDC Emulator WMI filter applied

DC Time Sync GPO with WMI filtering applied to PDC emulator.

Applying the Policies

Force replication so every domain controller has the new GPOs, then apply them and restart the time service.

Invoke-Command -ComputerName $Pdc -ScriptBlock { repadmin.exe /syncall /AdeP | Out-Null }
Start-Sleep -Seconds 60
Invoke-Command -ComputerName $DomainControllers -ScriptBlock {
gpupdate.exe /target:computer /force | Out-Null
Restart-Service -Name W32Time
w32tm.exe /resync /rediscover | Out-Null
}

Domain members pick up the new GPO at their next policy refresh. However, W32Time reads which providers are enabled only when the service starts, so a member VM keeps using VMICTimeProvider until W32Time restarts or the machine reboots. To switch a member over immediately, run the following commands on it.

gpupdate.exe /target:computer /force
Restart-Service -Name W32Time
w32tm.exe /resync /rediscover

Configuring the Workgroup Hyper-V Hosts

With the domain controllers fixed, the last piece is the Hyper-V hosts. In the customer’s environment, most of the domain ran on hosts that were workgroup members, and none had any time configuration. The VMs no longer take their time from the host continuously, but the host’s clock still sets each VM’s time at startup and after a restore from a saved state. Workgroup machines don’t participate in the domain hierarchy, so Group Policy can’t help here. Each host must be configured locally, and there are two reasonable options.

  • Sync directly from the public time service. This is what I did for the customer. The hosts don’t depend on a VM they’re running, which avoids a chicken-and-egg problem at boot. It does require allowing outbound UDP 123 from each host.
  • Sync from the PDC emulator. This keeps a single internal source of truth and needs no additional edge firewall rules, since domain controllers answer NTP requests by default. The downside is that the host boots before the PDC emulator VM and runs on its own clock until the domain controller is available.

Either way, everything traces back to the same UTC source. Run the following commands locally on each Hyper-V host.

# Sync from Cloudflare, with NIST as a fallback
# (0x1 = use SpecialPollInterval, 0x2 = fallback only, 0x8 = client mode)
w32tm.exe /config /manualpeerlist:"time.cloudflare.com,0x9 time.nist.gov,0xb" /syncfromflags:manual /update
# Poll every 1024 seconds (workgroup machines default to once a week)
Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\W32Time\TimeProviders\NtpClient' -Name SpecialPollInterval -Value 1024
Restart-Service -Name W32Time
w32tm.exe /resync /rediscover

Setting SpecialPollInterval is important. The workgroup default is 604,800 seconds, or one week, which is far too infrequent to keep a virtualization host accurate. The 0xb flag on time.nist.gov adds fallback only (0x2) to the same flags, so the hosts follow the same primary and fallback design as the PDC emulator. To sync from the PDC emulator instead, replace the peer list with the PDC emulator’s name or IP address, and optionally add another domain controller with the 0xb flag as a fallback. Use IP addresses if the hosts don’t use the domain’s DNS servers.

Domain Member Virtual Machines

VMICTimeProvider is enabled by default on every Hyper-V VM, not just domain controllers, so domain-joined member VMs take their time from the host instead of the domain hierarchy. With accurate hosts, that works, but it creates a second time root. Domain controllers follow the PDC emulator, while member VMs follow whichever host they run on. In my lab, a member VM was about 77 milliseconds off the PDC emulator, even though both its host and the PDC emulator synced with Cloudflare. If a host loses access to its time source, its VMs drift away from the domain controllers, which is the original problem in a new place. The Domain Member Time Settings GPO prevents this. It disables only the W32Time provider, not the Hyper-V time synchronization integration service, so VMs still get their clock set at startup and after a restore from a saved state.

Validating the Configuration

Once everything is in place, confirm each tier of the hierarchy is getting time from the right place, including the domain controllers, domain members, the Hyper-V hosts, and the domain as a whole.

Validating Domain Controllers

Run the following commands to check the time configuration and status of every domain controller.

Invoke-Command -ComputerName $DomainControllers -ScriptBlock {
Write-Output "=== $env:COMPUTERNAME ==="
w32tm.exe /query /configuration | Select-String 'Type:|NtpServer:|AnnounceFlags:' | ForEach-Object { $_.Line.Trim() }
w32tm.exe /query /status | Select-String 'Stratum:|Root Dispersion:|Source:' | ForEach-Object { $_.Line.Trim() }
Write-Output "VMICTimeProvider Enabled: $(Get-ItemPropertyValue -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\W32Time\TimeProviders\VMICTimeProvider' -Name Enabled)"
}
PowerShell output validating W32Time configuration on each domain controller

Validating domain controller time configuration settings.

The .Line.Trim() is there for a reason. Select-String results returned through Invoke-Command are deserialized objects that PowerShell can’t format, so without it you’ll see blank lines instead of the output. The following table shows what to look for on each type of server.

ServerTypeAnnounceFlagsSourceStratumVMICTimeProvider
PDC emulatorNTP (Policy)5 (Policy)time.cloudflare.com,0x840
Other domain controllersNT5DS (Policy)10 (Policy)PDC emulator50
Hyper-V hostsNTP (Local)n/atime.cloudflare.com,0x94n/a

The (Policy) label confirms Group Policy is in control rather than local settings. Root Dispersion will be high immediately after the service restarts and should settle under a second within about 30 minutes. NtpServer lists both time services, but the source should show time.cloudflare.com. If it shows time.nist.gov, the PDC emulator can’t reach Cloudflare and has switched to the fallback.

Validating Domain Members

On a domain member VM, run the following commands to confirm it follows the domain hierarchy and agrees with the PDC emulator.

w32tm.exe /query /source
w32tm.exe /query /status | Select-String 'Stratum:|Source:|ReferenceId:'
w32tm.exe /stripchart /computer:$Pdc /samples:5 /dataonly
w32tm.exe output on a domain member VM synchronizing with the PDC emulator dc1

Domain member VM synchronizing with the PDC emulator.

The source should be a domain controller, not VM IC Time Synchronization Provider, and the stratum should be one or two levels below the PDC emulator. In my lab, a member VM went from about 77 milliseconds off the PDC emulator to under 10 milliseconds within minutes of switching to the domain hierarchy.

Validating Hyper-V Hosts

On the Hyper-V hosts, run the following commands locally.

w32tm.exe /query /source
w32tm.exe /query /status | Select-String 'Stratum:|Root Dispersion:|Last Successful Sync Time:'
w32tm.exe /query /peers
w32tm.exe output on a Hyper-V host synchronizing with time.cloudflare.com, with time.nist.gov as a fallback peer

Time synchronization settings on the Hyper-V host server.

Both time services are listed as peers. The State for time.cloudflare.com should be Active. A state of Pending that persists usually means the host can’t reach that time server. However, Pending is also normal for up to one polling interval after the service restarts, which is about 17 minutes with a SpecialPollInterval of 1024 seconds, because W32Time rebuilds its peer list and resets the peer statistics. To tell the difference, run w32tm.exe /query /peers /verbose and check LastSyncError. A value of 0x00000000 (Succeeded) means the last sync worked, and the peer will return to Active at its next poll.

Validating the Domain

After things have had time to settle, check the entire domain at once. Every domain controller should show a small offset relative to the PDC emulator, with no errors.

w32tm.exe /monitor /domain:$DomainFqdn
w32tm.exe /monitor output showing all domain controllers in sync with the PDC emulator

All domain controllers synchronizing with the PDC emulator.

You might notice an unusual IPv4 address listed for dc1, such as 36.113.118.130. That’s not dc1’s address, and it’s not a real IPv4 address at all. It’s the RefID, which identifies the server’s upstream time source. The NTP reference ID field is only 32 bits, so when the source is reached over IPv6, as time.cloudflare.com is here, the field contains the first 32 bits of the MD5 hash of the source’s IPv6 address, as defined in RFC 5905. It’s expected behavior and can safely be ignored. In my lab, the PDC emulator went from roughly 840 milliseconds off UTC to closely tracking UTC once it began syncing with Cloudflare. In the customer’s environment, the Kerberos anomalies stopped once all domain controllers and hosts converged on a single source.

Summary

Time synchronization is easy to overlook, especially in virtualized environments where Hyper-V quietly fills in the gaps. In this case, the symptoms showed up as Kerberos anomalies during an AD CS engagement, but the root cause was a domain with no single, authoritative time source. The fix came down to the following steps.

  1. Reset every domain controller to the default W32Time configuration.
  2. Configure the PDC emulator, and only the PDC emulator, to sync with a reliable public time service, plus a second as a fallback, using a WMI-filtered GPO.
  3. Configure all other domain controllers to follow the domain hierarchy and disable VMICTimeProvider on every domain controller with a second GPO.
  4. Disable VMICTimeProvider on domain members with a third GPO linked to the domain root, so virtual machines follow the domain hierarchy too.
  5. Allow outbound UDP 123 for the systems that sync externally.
  6. Explicitly configure any workgroup Hyper-V hosts, since they don’t participate in the domain hierarchy.

With Group Policy managing the domain controllers and domain members, the configuration is consistent, self-documenting, and follows the PDC emulator role automatically if it ever moves.

Additional Information

Windows Time Service Technical Reference

Windows Time Service Tools and Settings

Synchronizing Time on Virtualized AD DS Environments: VMICTimeProvider vs AD

Configure the Root PDC with an Authoritative Time Source and Avoid a Widespread Time Skew

Cloudflare Time Services

NTP Pool Project

NIST Internet Time Service (ITS)

Network Time Protocol Version 4 (RFC 5905)

Managing Windows CLAT (WinCLAT) with Intune Remediations

Microsoft Intune icon

Windows CLAT (WinCLAT) allows IPv4 applications to reach IPv4 destinations on IPv6-only and IPv6-mostly networks. Windows does not permit WinCLAT on Wi-Fi or Ethernet by default, and Microsoft Intune does not currently expose a setting to permit it. This post explains how to deploy the permit setting with Intune Remediations, verify that WinCLAT is translating, and maintain connectivity to Intune when removing the setting.

WinCLAT Configuration Options in Intune

At the time of this writing, Intune does not expose WinCLAT configuration settings in the Settings Catalog or through a CSP. The corresponding Group Policy settings are defined in the built-in Windows template tcpip.admx. However, Intune’s custom ADMX import feature is intended for third-party and custom templates. Microsoft explicitly advises against importing built-in Windows ADMX files when the goal is to configure those settings, recommending the Settings Catalog or a custom profile instead.

Configuring WinCLAT with Remediations

An Intune Remediation can write the same registry value that Group Policy uses to permit WinCLAT on non-cellular interfaces. Windows reads that value as policy and gives it precedence over the local permit setting. If Group Policy already configures the setting on a device, continue managing it through Group Policy, since a policy refresh can overwrite the value written by the remediation script.

Deployment Requirements

Before deploying the Remediation, confirm that clients meet the following connectivity, platform, and licensing requirements.

Intune Connectivity Before WinCLAT Starts

The Intune Management Extension must be able to reach the endpoints it uses to retrieve the script package before WinCLAT starts. Native IPv4 provides that path on dual-stack networks and on IPv6-mostly networks where Windows still has a DHCPv4 lease. On an IPv6-only network, DNS64 and NAT64 can provide access to IPv4-only Intune endpoints by name. If the target network has no working path to the required endpoints before WinCLAT starts, deploy the package from a network that does. After deployment, verify that the device can still check in with Intune from the target network.

NetworkPath to Intune before WinCLAT is enabledExpected delivery path available?
Dual-stackNative IPv4Yes
IPv6-mostly (DHCPv4 option 108)Native IPv4. While WinCLAT is not permitted, Windows does not honor option 108 and takes a DHCPv4 leaseYes
IPv6-only with DNS64/NAT64DNS64 and NAT64Yes
IPv6-only without DNS64/NAT64No path to required IPv4-only endpoints in this scenarioNo

Windows and Intune Prerequisites

Devices must run Windows 11 version 25H2 build 26200.9550 or later, be Microsoft Entra joined or Microsoft Entra hybrid joined, and be enrolled in Intune or co-managed. Verify that device users have a qualifying license, such as Windows Enterprise E3 or E5, and that the Intune Management Extension is installed before running the package on demand.

WinCLAT Policy Settings

The WinCLAT Group Policy settings are in tcpip.admx under Computer Configuration > Policies > Administrative Templates > Network > TCPIP Settings > IPv6 Transition Technologies, and they are stored as DWORD values in the following location.

HKLM\SOFTWARE\Policies\Microsoft\Windows\TCPIP\v6Transition\IPxlatCfgSvc
Registry valuenetsh.exe settingModule propertyPurpose
PermitNonCellularCLATpermitPermitPermits WinCLAT on non-cellular interfaces
GetPrefixInfoFromRApref64fromraPref64FromRADiscovers the NAT64 prefix from the PREF64 option in router advertisements
GetPrefixInfoFromDNSpref64fromdnsPref64FromDnsDiscovers the NAT64 prefix by querying ipv4only.arpa against a DNS64 server

Interpreting WinCLAT Policy Values

Each policy value is a REG_DWORD. A value of 1 enables the setting, and a value of 0 disables it. Deleting the value returns control to the local setting. Windows allows NAT64 prefix discovery through both router advertisements and DNS by default, so the scripts in this post manage only PermitNonCellularCLAT.

Deploying the WinCLAT Remediation

Detect-WinClatPermitPolicy.ps1 compares PermitNonCellularCLAT with the desired registry state. It returns exit code 0 when the state matches and 1 when a change is needed. When detection returns 1, Intune runs Remediate-WinClatPermitPolicy.ps1 to write or remove the policy value.

Setting the Desired Policy State

Both scripts begin with a $DesiredState variable that corresponds to the three states of the Group Policy setting. Remediations do not accept parameters, so set the variable to the same value in both scripts before uploading them and create a separate Remediation for each state that is needed, such as Enable WinCLAT and Remove WinCLAT Policy.

# Enabled, Disabled, or NotConfigured. Must match the remediation script.
$DesiredState = 'Enabled'
Desired stateRegistry valueResult
Enabled1WinCLAT is permitted, and the setting is under policy control
Disabled0WinCLAT is not permitted, and the setting is under policy control
NotConfiguredDeleted, along with the IPxlatCfgSvc key if it is then emptyThe setting returns to the local configuration

Creating and Assigning the Package

Perform the following steps to create the Remediation in the Microsoft Intune admin center.

  1. Go to Devices > Manage devices > Scripts and remediations, then select Create script package.
  2. Enter a name, such as Enable WinCLAT, and upload the detection and remediation scripts with $DesiredState set to Enabled.
  3. Configure the following settings:
    1. Run this script using the logged-on credentials: No. The scripts write to HKLM and must run as SYSTEM.
    2. Enforce script signature check: No, unless the scripts are signed.
    3. Run script in 64-bit PowerShell: Yes.
  4. Assign the Remediation to a group of devices and choose a schedule. An hourly or daily schedule keeps the value in place if it is removed.
Intune Remediation script package settings

Intune Remediation script configuration settings.

Testing the Remediation on a Single Device

To test the package, open the device’s Overview page in the Intune admin center, select Run remediation, and choose the WinCLAT script package. You can run the package on demand without assigning it to the device. The device must be online, have the Intune Management Extension installed, and be able to communicate with Intune and the Windows Push Notification Service (WNS).

Run remediation action on an Intune device page

Run Remediation interactively on a specific endpoint.

Reviewing Remediation Results in Intune

On the device page in the Intune admin center, select Remediations to review the script package results. In the example below, With issues indicates that the registry state differed from the desired state, and Issue fixed indicates that the script corrected it. Verify the effective permit setting and translation state separately on the client.

Intune device Remediations results showing With issues and Issue fixed

Remediation results.

WinCLAT Activation After Remediation

Applying the permit policy allows Windows to start WinCLAT automatically on eligible interfaces. What happens next depends on whether the interface already has a native IPv4 address and whether Windows can discover a NAT64 prefix.

IPv6-Only Networks

On an IPv6-only test network, WinCLAT begins translating about a minute after the script writes the permit value, without a reboot, Group Policy refresh, or service restart. The instance uses an address from the IPv4 Service Continuity Prefix (RFC 7335), typically 192.0.0.1. Automatic activation requires an eligible interface with a Stateless Address Autoconfiguration (SLAAC) address, a discoverable NAT64 prefix, and autoenable enabled. The policy value persists across reboots.

IPv6-Mostly Networks

On an IPv6-mostly network, Windows already has a DHCPv4 lease when the remediation runs. Neither applying the permit policy nor running ipconfig /renew releases the existing lease. After the adapter restarts, Windows honors DHCPv4 option 108 (IPv6-Only Preferred), comes up without a native IPv4 address, and starts translating within seconds. For a production rollout, let devices reconnect normally or restart the target adapter during a maintenance window. Restarting it interrupts connectivity.

WinCLAT Activation Events

Use the Microsoft-Windows-IPxlatCfg/Operational log to verify that WinCLAT started. Event 1106 (Instance enabled) records why Windows enabled the instance, and Get-WinClatInstance reports that reason in TriggerSource when the event is available. When a new instance starts translating, event 1106 is typically followed by event 1108 (Remote prefix discovered) and event 1109 (Translation started). For more on event 1106 and how to identify the associated network adapter, see Understanding Windows CLAT (WinCLAT) Event Logging.

Event 1109 in the IPxlatCfg Operational log showing that WinCLAT translation started

Event 1109 shows that WinCLAT began translating.

Verifying Policy and Translation

Verify the effective permit setting first, then check whether WinCLAT is translating on the target interface. Use netsh.exe or the WinCLAT PowerShell module for both checks.

Verifying the Effective Permit Setting

Run netsh.exe interface clat show global store=both or Get-WinClatConfiguration -Store Both to compare the active and persistent stores. The active store shows the effective configuration, and the persistent store shows the saved local configuration. In this example, policy enables permit in the active store while the local setting remains disabled. Use the module’s PolicyControlled property to confirm that policy controls Permit, even when both stores report the same value.

Output of netsh interface clat show global store=both

Output of netsh.exe interface clat show global store=both.

Get-WinClatConfiguration -Store Both | Format-List
Output of Get-WinClatConfiguration -Store Both

Output of Get-WinClatConfiguration -Store Both.

Verifying Translation on an Interface

From an elevated command window, run netsh.exe interface clat show instance interface=<index> using the interface index from Get-NetAdapter, and look for Is translating : yes. Alternatively, run Get-WinClatInstance and look for IsTranslating : True on the target interface. The module lists active adapters with IPv6 enabled, so you can run it without supplying an interface index. To verify end-to-end connectivity, test an IPv4-only application or connect to a literal IPv4 address from the target network.

Output of netsh interface clat show instance

Output of netsh.exe interface clat show instance.

Output of Get-WinClatInstance

Output of Get-WinClatInstance.

Removing the WinCLAT Policy

Unassigning the enablement package leaves the policy value in the registry. To restore local control, create a removal package using the same scripts with $DesiredState set to NotConfigured. Use Disabled when you want policy to enforce a disabled permit setting.

Maintaining Intune Connectivity During Removal

Before removing the policy, confirm that the device will still be able to reach Intune if automatic WinCLAT translation stops. A device that relies on WinCLAT to reach IPv4-only Intune endpoints can lose its ability to report the result or receive a recovery package. Use a network with a working path to Intune through native IPv4 or DNS64 and NAT64. On an IPv6-mostly network, check whether the device has a native IPv4 address after removal. Reconnect the adapter if it needs to obtain a new DHCPv4 lease.

Replacing the Enablement Assignment

Before assigning the Remove WinCLAT Policy package, remove the target devices from the enablement assignment. Use a pilot device to confirm that it has received the assignment change and that the enablement package no longer runs on it. After the target devices report successful removal, you can assign the removal package more broadly.

Verifying Policy Removal

Run Get-WinClatConfiguration and confirm that PolicyControlled no longer lists Permit. The effective Permit value should now match the local setting, which is disabled by default. If removal stops an automatically enabled instance, Get-WinClatInstance should report IsTranslating : False for that interface. The operational log may also show event 1110 (Translation stopped) and event 1107 (Instance disabled).

Event 1107 showing that Windows disabled the WinCLAT instance

Event 1107 shows that Windows disabled the WinCLAT instance on the interface.

Get-WinClatConfiguration output showing Permit False and an empty PolicyControlled field

Get-WinClatConfiguration shows Permit=False and an empty PolicyControlled field.

Get-WinClatInstance output showing IsTranslating False

Get-WinClatInstance shows IsTranslating=False.

Limits of Registry-Based Management

The script packages manage the WinCLAT permit policy. Automatic activation, manually enabled instances, and local changes to policy-controlled settings require separate checks.

Automatic Activation

The autoenable setting is enabled by default and has no corresponding WinCLAT Group Policy setting. If an administrator has disabled it, setting PermitNonCellularCLAT to 1 permits WinCLAT but leaves automatic activation disabled. To restore automatic activation, use netsh.exe interface clat set global autoenable=enabled or Set-WinClatConfiguration -AutoEnable Enabled.

Manually Enabled Instances

Disabling Permit stops automatically enabled instances. A manually enabled instance can continue translating until you disable it explicitly or the device reboots. Check the instance state on the target interface if your objective is to stop translation.

Local Changes to Policy-Controlled Settings

When policy controls a WinCLAT setting, netsh.exe interface clat set global can report success and update the local value in the persistent store while the active value continues to follow policy. Use netsh.exe interface clat show global store=active to verify the effective value. If you use Set-WinClatConfiguration to request a value that differs from the effective policy value, the cmdlet reports PolicyOverride.

Using netsh.exe in a Remediation

A Remediation can also run netsh.exe to configure WinCLAT, and this is the only way to enforce autoenable. However, netsh.exe changes the local configuration rather than policy, and reliable detection requires parsing localized command output, so this approach is outside the scope of this post.

Summary

Intune Remediations can deploy and remove the WinCLAT permit policy on Windows 11. Make sure devices can reach Intune before deployment, then verify the effective policy and translation state on the target interface. On IPv6-mostly networks, devices with an existing DHCPv4 lease may need to reconnect before Windows uses WinCLAT. Before removing the policy, confirm that devices will still have a working path to Intune.

Additional Information

WinCLAT resources on directaccess.richardhicks.com

Troubleshooting Windows CLAT (WinCLAT) with the WinCLAT PowerShell Module

Windows CLAT (WinCLAT) is generally available on Windows 11 25H2, starting with build 26200.9550. WinCLAT is the Windows client-side translator for 464XLAT (RFC 6877). It translates IPv4 traffic from applications into IPv6 for delivery to a NAT64 gateway, allowing IPv4-only applications to continue working on IPv6-only and IPv6-mostly networks. Successful operation depends on both the Windows client configuration and the supporting network infrastructure. This post explains how to use the WinCLAT PowerShell module to verify these requirements, diagnose common problems, and test IPv4 connectivity through the translation path.

Why Use the WinCLAT PowerShell Module

WinCLAT is configured and inspected using the netsh.exe interface clat command context. However, its output requires careful interpretation when troubleshooting. A configuration command can complete without error even when Group Policy prevents the requested setting from taking effect. An instance can report that it is translating while IPv4 traffic fails, and disabling a global setting does not necessarily stop a manually enabled instance. The WinCLAT PowerShell module simplifies these tasks by checking the effective configuration after changes and providing health checks with explanations and recommended corrective actions. It supports troubleshooting on individual computers and across multiple remote computers.

How WinCLAT Activates

Understanding when WinCLAT activates automatically helps explain many troubleshooting scenarios. With AutoEnable enabled, Windows automatically activates WinCLAT on eligible non-cellular interfaces when the following conditions are met.

  • WinCLAT is permitted. The Permit setting is enabled. Today, it is disabled by default in Windows.
  • The interface has an IPv6 address assigned using Stateless Address Autoconfiguration (SLAAC) and an IPv6 default route.
  • The interface has no native IPv4 connectivity. On an IPv6-mostly network, DHCPv4 option 108, called IPv6-Only Preferred (RFC 8925), tells a compatible client to operate without a native IPv4 address for the period specified by the DHCPv4 server. WinCLAT provides IPv4 application connectivity over IPv6 during that time.
  • A NAT64 prefix has been discovered, either from the PREF64 option in router advertisements (RFC 8781) or by querying ipv4only.arpa for AAAA records through a DNS64 server (RFC 7050). DNS64 is required for the DNS-based discovery method but is not required when WinCLAT discovers the prefix through PREF64.

NAT64 Gateway Requirements

Successful IPv4 connectivity through WinCLAT also requires a reachable NAT64 gateway configured to translate traffic using the discovered prefix. Discovering a prefix and activating WinCLAT do not confirm that the gateway is reachable or that traffic can pass through it.

Automatic and Manual Activation

AutoEnable is enabled by default and allows Windows to activate WinCLAT automatically when an interface meets the required conditions. Manually enabled instances behave differently, as described later in this post. WinCLAT can be enabled manually on a specific interface after automatic activation is disabled. When WinCLAT is translating, Windows assigns the interface an IPv4 address from the 192.0.0.0/29 IPv4 Service Continuity Prefix (RFC 7335) and adds an IPv4 default route through WinCLAT.

Installing the Module

The WinCLAT module is available in the PowerShell Gallery and runs on Windows PowerShell 5.1 and PowerShell 7. Run the following command to install the module.

Install-Module -Name WinCLAT -Scope CurrentUser

The module is needed only on the computer running the commands. Remote computers require WinRM, and commands that read interface state or change configuration require an elevated session on the target.

Step 1: Run Test-WinCLAT

Test-WinClat is the starting point for any WinCLAT problem. Run it from an elevated session so the interface checks can run.

Test-WinClat

The summary shows the overall result (Pass, Warning, Fail, or NotApplicable), whether any interface is translating, and the checks that returned a warning or failure. Individual checks can also return Info for conditions that do not affect the overall result. To display every check and its explanation, use -Detailed with Format-List.

Test-WinClat -Detailed | Format-List -Property Check, InterfaceAlias, Result, Detail

To show only the checks that need attention, including recommended corrective actions, run the following command.

Test-WinClat -Detailed | Where-Object -FilterScript { $_.Result -in 'Fail', 'Warning' } | Format-List -Property Check, InterfaceAlias, Detail, Remediation

Test-WinClat Tests

The following is a list of checks that Test-WinClat performs.

TestWhat it verifies
Feature presentThe clat netsh.exe context exists on the computer
CLAT permittedPermit is enabled in the active (effective) configuration
Prefix discovery validAt least one of Pref64FromRA and Pref64FromDns is enabled
Policy overrideGroup Policy is not overriding the local configuration
SLAAC addressThe interface has an IPv6 address from SLAAC
IPv6 default routeThe interface has an IPv6 default route
Native IPv4The interface has no native IPv4 address
Instance activeWinCLAT is translating when its prerequisites are met
CLAT addressThe WinCLAT IPv6 address is assigned to the interface and on its current prefix
Prefix consistencyThe NAT64 prefix matches the prefix returned by DNS64, when DNS64 is available
Mode sanityWinCLAT can activate: AutoEnable is enabled, or an instance has been enabled manually
Functional testIPv4 traffic passes through WinCLAT (optional)

Global checks do not require elevation. Interface checks are skipped, rather than failed, when the session is not elevated. Results of Info are informational and do not affect the overall result.

Step 2: Inspect the Configuration

Get-WinClatConfiguration returns the global settings. Use -Store Both to compare the active (effective) configuration with the persistent (local) configuration.

Get-WinClatConfiguration -Store Both

Compare the active configuration with the persistent configuration to identify differences between the effective settings and the locally stored settings. Use the PolicyControlled property to identify settings controlled by Group Policy. This property also identifies policy-controlled settings whose effective values match the local values, which a comparison of the two stores alone would not reveal.

Step 3: Inspect the Instances

Get-WinClatInstance returns the WinCLAT state of each IPv6-enabled interface. It requires elevation.

Get-WinClatInstance

Two properties are particularly useful when troubleshooting:

  • Mode shows whether the instance was enabled Automatically by Windows or Manually by an administrator.
  • TriggerSource shows why the instance was enabled: Pref64 RA (the network advertises PREF64), No IPv4 (the interface has no IPv4 connectivity), or Manual. It is read from event 1106 in the Microsoft-Windows-IPxlatCfg/Operational event log.

Common Problems

The following are common problems associated with WinCLAT configuration and operation.

Feature Not Present

Get-WinClatConfiguration returns Status FeatureNotAvailable, and Test-WinClatfails the Feature present check. WinCLAT is generally available on Windows 11 25H2 build 26200.9550 and later. The OSBuild property shows each computer’s build number, making it easy to identify computers that need the latest cumulative update.

WinCLAT is not available on Windows Server. Servers return Status NotSupported, and Test-WinClat reports an overall result of NotApplicable.

WinCLAT Not Permitted

Permit is disabled by default, so WinCLAT does not activate automatically on Wi-Fi or Ethernet interfaces until Permit is enabled, either locally or through Group Policy. Test-WinClat fails the CLAT permitted check only when an interface is IPv6-only and could use WinCLAT. On a dual-stack network, the check is informational because WinCLAT would not be used there anyway.

With AutoEnable enabled, WinCLAT activates on eligible interfaces as soon as Permit is enabled.

DHCPv4 Option 108

Permit also affects DHCPv4 option 108. Windows honors option 108 only while WinCLAT is permitted. With Permit disabled, a client on an IPv6-mostly network accepts a DHCPv4 lease and uses native IPv4, so WinCLAT is not needed, and the CLAT permitted check is informational.

WinCLAT Not Active on Dual-Stack Network

This is expected. WinCLAT does not activate on an interface with native IPv4. Test-WinClatreports Native IPv4 and Instance active as Info on dual-stack interfaces. If a dual-stack interface has no SLAAC address or no IPv6 default route, as on a network that assigns IPv6 addresses with DHCPv6 only, those checks are also Info rather than Fail, because they matter only if the network becomes IPv6-only.

SLAAC Addressing Not Present

WinCLAT requires an IPv6 address configured using SLAAC. An interface whose IPv6 addresses are assigned only through DHCPv6 does not meet this requirement. DHCPv6 can still provide other configuration information, such as DNS server addresses and a domain search list. Test-WinClat checks for a SLAAC address and reports when an interface that needs WinCLAT does not have one.

Group Policy Overrides Local Configuration

Netsh.exe reports success when a local change is made to a setting that Group Policy controls, but the effective value does not change. Set-WinClatConfiguration reads the configuration back after every change and reports the result with Status PolicyOverride when the policy keeps a different value.

In addition, Test-WinClat indicates when local settings conflict with Group Policy enforced settings.

Group Policy can control Permit, Pref64FromRA, and Pref64FromDns. There is no policy for AutoEnable. The policy values are stored in the following registry location.

HKLM\SOFTWARE\Policies\Microsoft\Windows\TCPIP\v6Transition\IPxlatCfgSvc

No NAT64 Prefix Discovered

An instance that is enabled but has no NAT64 prefix cannot translate. Test-WinClat fails the Instance active check and names the prefix discovery methods that are enabled. Common causes include:

  • The network does not include the PREF64 option in router advertisements, and Pref64FromDns is disabled.
  • No usable NAT64 prefix is available through router advertisements, and Pref64FromDns is enabled, but the DNS servers assigned to the client do not provide DNS64.
  • Both Pref64FromRA and Pref64FromDns are disabled. Netsh.exe accepts this combination silently, but WinCLAT can never activate. Set-WinClatConfiguration refuses it.

When WinCLAT discovers a NAT64 prefix through the PREF64 option in router advertisements, DNS64 is not required for prefix discovery or for translating IPv4 application traffic. A reachable NAT64 gateway using that prefix is still required. If DNS64 is unavailable, the Prefix consistency check is informational because there is no DNS64-derived prefix to compare.

Delayed WinCLAT Activation

WinCLAT activates fastest when the network advertises the NAT64 prefix through PREF64. When DNS64 is the only available prefix discovery method, Windows first waits to determine that the interface has no native IPv4 connectivity, so activation takes several seconds longer. Allow time for this process before treating an inactive instance as a configuration problem.

Manually Enabled Instances Continue Translating

Disabling Permit or AutoEnable stops translation on instances that Windows enabled automatically. Manually enabled instances can continue translating after either setting is disabled. When troubleshooting continued translation, check the instance’s Mode property to determine whether it was enabled manually.

Use Get-WinClatInstance to identify manually enabled instances.

Get-WinClatInstance | Where-Object -FilterScript { $_.Enabled -and $_.Mode -eq 'Manual' }

Note that netsh.exe refuses to change an instance while AutoEnable is enabled. To return an interface from manual to automatic control, disable AutoEnable, disable the instance, and enable AutoEnable again:

Set-WinClatConfiguration -AutoEnable Disabled
Set-WinClatInstance -InterfaceAlias 'Ethernet' -State Disabled
Set-WinClatConfiguration -AutoEnable Enabled

WinCLAT Stops Working After Reboot

Manual instance state is runtime only. It survives an adapter restart and link loss, but it is lost on reboot. Enabling an instance manually requires disabling AutoEnable, and AutoEnable remains disabled after the reboot, so WinCLAT does not activate at all. Enable AutoEnable again to restore automatic operation:

Set-WinClatConfiguration -AutoEnable Enabled

The Mode sanity check in Test-WinClat reports this condition.

IPv4 Fails After Moving to a Different Network

When a translating client moves to a different IPv6 network without restarting its network adapter, WinCLAT keeps its IPv6 address on the previous network’s prefix. The instance still reports that it is translating, but IPv4 traffic through WinCLAT fails. Microsoft has acknowledged this known issue and is working to resolve it in a future release.

Test-WinClat detects this condition. The CLAT address check fails, naming the address in use and the current prefix, while the Instance active check passes.

Restarting the network adapter resolves the issue.

Restart-NetAdapter -Name 'Ethernet'

The IPv6 address reported by netsh.exe interface clat show instance cannot be used to detect this condition, because it may report a new address that is not actually assigned to the interface. The CLAT address check compares the addresses actually assigned to the adapter with its current prefix.

Netsh.exe Reports Addresses That Are Not Assigned

An instance that is enabled but no longer translating can retain stale information in netsh.exe output. For example, a manually enabled instance on an interface that now has native IPv4 connectivity might still report a WinCLAT IPv6 address and 192.0.0.1 even though those addresses are no longer assigned to the interface. It might also display a NAT64 prefix with a remaining lifetime of zero. Use Get-NetIPAddress to confirm the addresses assigned to the interface. Clear the manual instance using the three-command sequence shown earlier or restart the computer.

Testing the Translation Path

The checks above assess WinCLAT configuration and local instance state. The functional test verifies connectivity to a specific IPv4 destination through WinCLAT and the NAT64 gateway. Use Test-WinClat with an IPv4 target address that is reachable through the gateway and known to respond to the selected test.

Test-WinClat -IPv4Target 172.16.0.204 -Detailed | Format-Table -Property Check, InterfaceAlias, Result, Detail -Wrap

The test sends up to three ICMP echo requests. Add -IPv4Port to test a TCP connection instead, which is useful where ICMP is filtered.

Test-WinClat -IPv4Target 172.16.0.204 -IPv4Port 443

Keep the following in mind when interpreting the result.

  • Use an IPv4 address as the target. A hostname can resolve to a synthesized AAAA record through DNS64, allowing the connection to use IPv6 through NAT64 without using WinCLAT. Testing with an IPv4 address ensures the test uses IPv4. Check the selected interface and source address to confirm that the traffic passes through WinCLAT.
  • Native IPv4 can bypass WinCLAT. If another interface provides native IPv4 connectivity to the test destination, Windows might send the test traffic through that interface. The module reports Info when the selected path bypasses WinCLAT, even if the destination responds successfully. Review the Detail property to identify the selected interface and source address and confirm whether the test used WinCLAT.
  • Unreachable destinations time out. ICMP destination unreachable messages that NAT64 returns for IPv4 destinations are not delivered to the IPv4 application, so a test to an unreachable or filtered address returns a Warning after timing out rather than an immediate failure. Choose a target that is known to respond, ideally on your own infrastructure.
  • One test covers one path. When several interfaces translate, the test proves that IPv4 works through the interface Windows chose, not on each interface.

Troubleshooting at Scale

The WinCLAT commands accept computer names from the pipeline, including input from Get-Content and Get-ADComputer, and process remote computers in parallel over WinRM. Before connecting, the module checks TCP port 5985. If that connection fails, it reports the computer as Unreachable without waiting for a full WinRM timeout. This status indicates that the module could not reach the required endpoint. The computer might be offline, the port might be blocked, or WinRM might be configured to use a different TCP port.

Filtering and Exporting Results

The module returns a result for every target computer, including those that cannot be reached, do not support the feature, or cannot be fully checked because the session lacks elevation. The Status property identifies the outcome, and the Message property explains it.

# Test every computer in an OU and show only the computers with problems
Get-ADComputer -Filter * -SearchBase 'OU=Windows Clients,DC=corp,DC=example,DC=net' | Test-WinClat | Where-Object -FilterScript { $_.Overall -ne 'Pass' }
# Export the detailed results of every check for later analysis
Get-Content -Path .\clients.txt | Test-WinClat -Detailed | Export-Csv -Path .\winclat-results.csv -NoTypeInformation
# Save the names of unreachable computers to test again later
Get-Content -Path .\clients.txt | Test-WinClat | Where-Object -FilterScript { $_.Status -eq 'Unreachable' } | Select-Object -ExpandProperty ComputerName | Set-Content -Path .\retry.txt

The output is flat, so it exports cleanly with Export-Csv for reporting and comparison over time.

Known Issues

The following Windows behaviors can affect WinCLAT operation and troubleshooting. The module can identify some of these conditions, but it does not correct the underlying Windows behavior. Available workarounds are listed below.

IssueEffectWorkaround
WinCLAT keeps its IPv6 address on the previous prefix after a network changeIPv4 traffic through WinCLAT fails; Test-WinClat reports CLAT address FailRestart the network adapter
WinCLAT IPv4 address is dynamically registered in DNSConnections to the client may fail when DNS resolves its name to a WinCLAT IPv4 addressNone
Translated ICMP errors are not delivered to IPv4 applicationsUnreachable IPv4 destinations time out instead of failing immediatelyNone

Summary

Troubleshooting WinCLAT starts with Test-WinClat, which checks every prerequisite, explains each failure, and suggests remediation. Get-WinClatConfiguration reveals Group Policy overrides, and Get-WinClatInstance shows whether each instance was enabled automatically or manually. Most problems come down to a few causes: Permit disabled by default, Group Policy overriding local settings, no NAT64 prefix discovered, a manual instance that does not follow the global settings, or a stale WinCLAT address after a network change. Because netsh.exe reports success even when a change does not take effect, always verify the result by reading the state back, which the WinCLAT module does automatically.

Additional Information

WinCLAT PowerShell Module on GitHub

WinCLAT on directaccess.richardhicks.com

Entra Global Secure Access (GSA) Client Intune Deployment PowerShell Script

The Microsoft Entra Global Secure Access (GSA) client is commonly deployed using Microsoft Intune. To deploy the client as an Intune Win32 app, administrators package the installer and a PowerShell installation script into an .intunewin file. Microsoft provides a sample PowerShell script that creates a log, configures Windows to prefer IPv4 over IPv6, and launches the installer. However, the sample has several limitations that can make it unreliable in production environments. To address these shortcomings, I’ve refactored the code to make it more robust and better aligned with enterprise deployment best practices.

Script Improvements

Microsoft’s PowerShell script for installing the GSA client has several significant limitations. My refactored version includes the following improvements.

  1. Preserved the existing DisabledComponents setting. Microsoft’s example overwrites the entire registry value, potentially removing previously configured settings. My version sets the 0x20 bit to prefer IPv4 while preserving any other flags already present.
  2. Validates the installer’s Authenticode signature. My revised script verifies that the installer has a valid Authenticode signature and that the signing certificate identifies Microsoft Corporation as the publisher.
  3. Preserves the pending reboot state. My script records when the registry setting was changed and compares that timestamp with the device’s last boot time. If the device has not restarted, subsequent runs continue to return exit code 3010, even if an earlier installation attempt failed.
  4. Expanded exit-code handling. The script writes a timestamped PowerShell transcript to the Intune Management Extension log directory, allowing it to be included with collected Intune diagnostics. Logging failures do not prevent the installation from continuing.
  5. Intune-integrated logging. I’ve updated the script’s logging to use the Intune Management Extensions logs folder, so logs are captured by Intune’s diagnostics collection. Each run of the script generates its own timestamped log file, making troubleshooting easier. Also, logging failures never block installation.
  6. Best practice alignment. I’ve added comment-based help, culture-invariant timestamps, and a $PSScriptRoot guard with a clean error to handle script execution issues. In addition, the script is digitally signed to support environments that enforce signed-script execution policies.

GitHub

I’ve published my refactored GSA client installation script on GitHub. You can download the script here:

https://github.com/richardhicks/gsa/blob/main/Install-GSAClient.ps1

Note: The script expects the installer to be named GlobalSecureAccessInstaller.exe. However, downloaded installers typically include the version number in the filename, such as GlobalSecureAccessInstaller_<version>.exe. Before creating the .intunewin package, either rename the installer or update the $InstallerName variable in the script.

Contribute

Suggestions and contributions are welcome. If you have ideas for making the GSA client deployment more robust or reliable, please submit a pull request on GitHub.

Summary

Microsoft provides a PowerShell script to deploy the Entra GSA client via Intune, but it has several limitations. This refactored version preserves existing registry settings, validates the installer, improves reboot and exit-code handling, integrates logging with Intune diagnostics, and better aligns with enterprise deployment best practices.

Additional Information

Install-GSAClient.ps1 PowerShell Script on GitHub

Prepare Win32 App Content for Upload to Microsoft Intune

Install the Global Secure Access Client for Microsoft Windows

Intranet Certificate Monitoring with CertKit

CertKit is a cloud-based solution that automates the issuance and management of public TLS certificates. It also provides certificate monitoring and alerting for public-facing workloads. However, many organizations have internal (non-public-facing) services that also use TLS. CertKit agent v1.13.0, currently in preview, closes this gap by providing visibility into TLS certificates used by intranet resources. After installing the latest release of the agent, CertKit can now provide visibility into the status of TLS certificates for internal resources.

Intranet Monitoring

After installing or updating the CertKit agent to v1.13.0, administrators can select an internal TLS service to monitor. Follow the steps below to enable this feature.

Certificate Collection

I recommend creating a separate Certificate Collection to support intranet monitoring. In the CertKit management console, click Add Collection, enter a unique, descriptive name, and click Add Collection.

Install the Agent

Once the new collection is created, navigate to the Agents tab, select your platform, then download, install, and register the agent.

Monitor New Host

After the agent is installed and registered, navigate to the Monitoring tab and click Monitor New Host. Enter the fully qualified domain name (FQDN) of the resource and specify its TLS port. Under Host Visibility, select Internal, and then choose the agent to monitor the host from. When finished, click Monitor Host.

Note: The server running the CertKit agent does not require a TLS certificate of its own to monitor internal resources.

Monitored Hosts

CertKit can track certificates for any internal workload that uses TLS. In this example, the monitored resources include web applications, security appliances, load balancers, and HTTP CRL distribution points. The list also includes LDAPS on the domain controllers and the RDP and WinRM HTTPS services on the management workstation.

The circle next to each resource indicates its certificate status. Green indicates a healthy certificate, yellow indicates one approaching expiration, and red indicates an expired certificate or another detected issue.

Note: The RDP certificate on the management workstation appears red because it contains only the Remote Desktop Authentication EKU (1.3.6.1.4.1.311.54.1.2). It intentionally does not include Server Authentication, which CertKit currently expects when validating the service. CertKit is aware of this limitation and plans to address it in a future release.

Host Discovery

CertKit provides robust discovery for public websites but does not currently offer equivalent functionality for intranet resources. Administrators can identify internal systems listening on a specific port by using Nmap.

nmap.exe -Pn -p [port] --open [internal subnet]

For example:

nmap.exe -Pn -p 443 --open 172.16.0.0/24

Alternatively, add the -oX switch to save the output in XML format.

nmap.exe -Pn -p 443 --open 172.16.0.0/24 -oX scan.xml

The resulting Nmap XML file output can be converted to CSV format using this PowerShell code.

Summary

CertKit agent v1.13.0 extends certificate monitoring to internal TLS services, giving administrators greater visibility into certificates that were previously difficult to track. Although intranet host discovery remains a manual process, tools such as Nmap and PowerShell can help identify resources to add to the monitoring platform

Getting Started with CertKit

Need help improving certificate visibility and management across your organization? Want to automate public TLS certificate enrollment for workloads such as DirectAccess, Always On VPN, IIS, SQL, and more? I can help you assess your certificate environment, identify monitoring gaps, and develop a strategy for managing certificates across internal and public-facing workloads. Fill out the form below, and I’ll provide you with more information.

← Back

Thank you for your response. ✨

Additional Information

CertKit Website

What Is CertKit?

CerKit Agent Support for Always On VPN SSTP and DirectAccess IPHTTPS TLS Certificates

IIS TLS Certificate Deployment with CertKit

The Case for 6-Day Public TLS Certificates

DirectAccess IPHTTPS and Let’s Encrypt 6-Day TLS Certificates

NDES Security Hardening: Protecting and Monitoring the SCEP Registry Configuration

The Active Directory Certificate Services (AD CS) Network Device Enrollment Service (NDES) is widely deployed to support the issuance and management of enterprise PKI certificates via Microsoft Intune and the Intune Certificate Connector. NDES relies on registry settings to identify its issuing CA, certificate templates, and other SCEP configuration parameters. An attacker with sufficient access to an NDES server could alter these settings and redirect enrollment to another template published on the issuing CA and accessible to the NDES service account. Because enrollment might continue without an obvious service failure, administrators could overlook the change. This article explains how to restrict access to the MSCEP registry key, audit modifications, and use Sysmon to collect additional investigative context.

Understanding the MSCEP Registry Configuration

NDES stores its core SCEP configuration under the HKLM\SOFTWARE\Microsoft\Cryptography\MSCEP registry key. These settings identify the issuing CA and certificate templates used for enrollment and control other aspects of NDES operation. Because they directly influence how certificate requests are processed, unauthorized changes could alter enrollment behavior without modifying the NDES application itself.

Enrollment Templates

NDES uses the following registry values to identify the certificate templates used for SCEP enrollment.

HKLM\SOFTWARE\Microsoft\Cryptography\MSCEP\EncryptionTemplate
HKLM\SOFTWARE\Microsoft\Cryptography\MSCEP\GeneralPurposeTemplate
HKLM\SOFTWARE\Microsoft\Cryptography\MSCEP\SignatureTemplate

MSCEP registry key with the EncryptionTemplate, GeneralPurposeTemplate, and SignatureTemplate values

In many deployments, all three values reference the same template. If the environment supports distinct signature, encryption, or general-purpose enrollment scenarios, each value can reference a template designed for that specific purpose.

Additional MSCEP Settings

The MSCEP key contains more than certificate template assignments. Its CAInfo and CAType subkeys identify the CA to which NDES submits requests, while other subkeys control settings such as the hash algorithm and capabilities advertised to SCEP clients.

Information Disclosure and Reconnaissance

The default permissions for the MSCEP registry key are inherited from HKLM\SOFTWARE, which grants the local Users group read access. This allows locally authenticated users, as well as users granted remote registry access, to read the template names and CA configuration. Template names can aid reconnaissance, as an attacker can query Active Directory for the corresponding template definitions and evaluate their configurations against known AD CS escalation techniques. The CAInfo subkey can also disclose the internal fully qualified domain name of the issuing CA. Restricting access to the MSCEP key reduces unnecessary exposure of this information.

Why the MSCEP Key Is an Effective Monitoring Target

After NDES is deployed to production, these values should change only during planned reconfiguration. This makes the MSCEP key a high-value, low-noise monitoring target. Legitimate modifications should be rare and documented, while any unexpected change warrants immediate investigation.

Before You Begin

The changes described in this article modify access permissions and configure auditing for registry settings that NDES requires. An incorrect service identity, incomplete allowlist, or unsupported environmental assumption can prevent certificate enrollment. Test these changes in a nonproduction environment first, confirm that you have a working backup and rollback procedure, and validate successful enrollment before applying them broadly. Adapt the examples to your environment and follow your organization’s change management and security review processes.

In addition, be sure to back up the configuration before making any changes. A full server backup or a virtual machine snapshot is helpful. However, administrators can perform additional steps to save information before altering the default settings.

Determine the NDES Service Account

The permissions applied below grant access to the account used by the SCEP application pool. Confirm this account before proceeding by opening IIS Manager (inetmgr.exe), expanding the IIS server, and selecting Application Pools. Highlight the SCEP application pool and note the account shown in the Identity column.

IIS Manager application pools showing the SCEP application pool identity

Back Up the Current Permissions

A registry export with reg.exe captures data but not the security descriptor, so it is not a rollback for this change. Run the following PowerShell commands to capture the descriptor in SDDL format instead.

$Key = 'HKLM:\SOFTWARE\Microsoft\Cryptography\MSCEP'
(Get-Acl -Path $Key -Audit).Sddl | Out-File -FilePath .\mscep-acl-backup.txt -Encoding utf8

The rollback process is covered later in this post.

Restricting Access to the MSCEP Key

For a dedicated NDES server, restrict the MSCEP key to SYSTEM, the local Administrators group, and the identity used by the SCEP application pool. All other access entries should be removed, including the inherited Read permission for the local Users group. If another service identity requires access in your environment, such as a domain service account used by the Intune Certificate Connector, account for it in both the explicit access rules and the allowlist before applying the changes.

Administrators must retain full control. Even if the Administrators entry were removed, a local administrator could exercise the necessary privileges to take ownership of the key and rewrite its permissions. The ACL therefore reduces unnecessary access but does not provide a security boundary against a local administrator or an attacker with equivalent privileges. This is why the auditing described in the next section is an important complementary control.

Important Note: Registry values do not carry their own security descriptors, so the permissions applied here govern the MSCEP key, every value in it, and every subkey beneath it. The service identity is granted full control rather than read because NDES writes to this key during normal operation, and read-only access will break it.

Removing Inheritance

The inherited entries from HKLM\SOFTWARE are where the broad access comes from, so they must be removed first. On the NDES server, open an elevated PowerShell command window and run the following commands.

$Acl = Get-Acl -Path $Key -Audit
$Acl.SetAccessRuleProtection($True, $False)

The -Audit parameter reads the system access control list (SACL) and the discretionary access control list (DACL), so both can be updated and committed in a single write later. The second parameter of SetAccessRuleProtection is set to false, which discards the inherited entries rather than converting them to explicit copies. This approach is intentional. Inherited entries cannot be removed with the PurgeAccessRules method, so preserving them would leave the broad grants in place and defeat the entire exercise. No explicit deny entry is required anywhere in this process. After this change, the local Users group has no access to the key at all.

Granting Explicit Access

Breaking inheritance also removes the SYSTEM and Administrators grants, so our explicit entries will replace them. Well-known security identifiers (SIDs) are used instead of account names, so this works correctly on non-English Windows installations.

$ServiceIdentity = 'corp\gmsa_ndes$'   # or 'corp\svc_ndes' if using a domain service account
$Sid = (New-Object System.Security.Principal.NTAccount($ServiceIdentity)).Translate([System.Security.Principal.SecurityIdentifier])
ForEach ($Identity in @('S-1-5-18', 'S-1-5-32-544', $Sid.Value)) {
    $Trustee = New-Object System.Security.Principal.SecurityIdentifier($Identity)
    $Acl.AddAccessRule((New-Object System.Security.AccessControl.RegistryAccessRule($Trustee, 'FullControl', 'ContainerInherit', 'None', 'Allow')))
}

S-1-5-18 is the well-known SID for SYSTEM, and S-1-5-32-544 is the well-known SID for the local Administrators group. ContainerInherit ensures the rights propagate to the subkeys.

Removing Unapproved Access Entries

The remaining explicit entries are evaluated against an allowlist, and anything that does not belong to those three identities is purged.

$AllowedSids = @('S-1-5-18', 'S-1-5-32-544', $Sid.Value)
ForEach ($Entry in ($Acl.Access | Where-Object { -not $_.IsInherited } | Select-Object -ExpandProperty IdentityReference -Unique)) {
    If ($Entry -is [System.Security.Principal.SecurityIdentifier]) {
        $EntrySid = $Entry
}
Else {
        $EntrySid = $Entry.Translate([System.Security.Principal.SecurityIdentifier])   
}
   
If ($AllowedSids -notcontains $EntrySid.Value) {
        $Acl.PurgeAccessRules($EntrySid)   
}
}

Sweeping against an allowlist rather than purging a fixed list of known principals removes application capability SIDs, CREATOR OWNER, any application pool entry that no longer matches the current SCEP application pool service account, and anything left over from previous administrative changes, without enumerating them in advance. It also makes the operation repeatable, so running it a second time on an already hardened server produces the same result.

Important Note: This code uses SID comparison. Identity references can be returned as either an NTAccount or a SecurityIdentifier, depending on whether the account resolves, so comparing by name will silently miss orphaned entries.

Auditing Changes to the MSCEP Key

Restricting the ACL reduces unnecessary access, but it cannot prevent a local administrator from taking ownership or rewriting the permissions. Auditing provides a complementary detective control by recording attempts to modify the key.

Add the Registry Audit Rule

To enable auditing, add an audit rule to the MSCEP key’s SACL that records successful and failed attempts by any identity to modify its values, subkeys, or permissions. Then commit the DACL and SACL together.

$Everyone = New-Object System.Security.Principal.SecurityIdentifier('S-1-1-0')
$AuditRule = New-Object System.Security.AccessControl.RegistryAuditRule($Everyone, 'SetValue, CreateSubKey, Delete, ChangePermissions, TakeOwnership', 'ContainerInherit', 'None', 'Success, Failure')
$Acl.AddAuditRule($AuditRule)
Set-Acl -Path $Key -AclObject $Acl

The audited operations are limited to changes. Specifically, setting a value, creating or deleting a subkey, changing permissions, and taking ownership. Reads are intentionally not audited. The three identities that still have access read these values routinely, so auditing that activity would generate excessive noise. Note that failure auditing applies only to the listed modification rights, so a denied read attempt will not be logged.

In addition, TakeOwnership and ChangePermissions are included deliberately because an attempt to alter the key’s security descriptor could indicate that someone is trying to weaken or bypass the applied controls. These operations should be rare after deployment and warrant investigation when they are not associated with an approved administrative change.

Recycle the SCEP Application Pool

Once the permissions are committed, recycle the SCEP application pool to close any existing registry handles and ensure the worker process reopens the MSCEP key under the new access controls.

Restart-WebAppPool -Name SCEP

Enable the Registry Audit Subcategory

An audit rule on its own does not generate events. The Object Access category must have the Registry subcategory enabled before Windows will write anything to the Security event log. Confirm the current setting by running the following command.

auditpol.exe /get /subcategory:"{0CCE921E-69AE-11D9-BED3-505054503030}" /r

The subcategory is referenced by GUID rather than by name for locale independence. If it is not enabled, enable it through Group Policy under Advanced Audit Policy Configuration > Object Access > Audit Registry rather than setting it locally, so the setting survives and applies consistently across your NDES infrastructure.

Recommended Security Alerts

With the Registry audit subcategory and the SACL configured as described, creating, deleting, or modifying a registry value under the MSCEP key generates event ID 4657 in the Security event log, identifying the account, the value name, and both the old and new data. That last detail is what makes the event immediately actionable. You do not have to look up what the template was supposed to be, because the event tells you what it was and what it was changed to.

My recommendation is to alert on every event ID 4657 associated with EncryptionTemplate, GeneralPurposeTemplate, or SignatureTemplate values rather than attempting to tune out individual processes or accounts. These values should remain static after deployment, so their expected change volume is zero. Monitor modifications elsewhere under the MSCEP path as well but establish a baseline before treating every such event as an alert because some NDES configurations may update other values or subkeys during normal operation.

Additional Alerts

Two additional events are worth correlating when one of these fires:

  • Event ID 4670 records a change to the permissions on the object. On a hardened key, this is a strong signal, since it means someone modified the ACL you just applied.
  • An application pool recycle or an IIS restart shortly after a value change is the corroborating signal. Because NDES caches the template configuration in the worker process, a substituted template does not take effect until the SCEP application pool or server restarts. An attacker seeking immediate activation would therefore need to trigger a recycle or restart, creating another potentially observable event. Otherwise, the change could take effect later during a routine recycle or restart.

Add Process Context with Sysmon

Security event ID 4657 identifies the account, process name, process ID, registry value, and old and new data associated with a change. However, it does not provide the full command line, process lineage, hashes, or surrounding activity needed for a more complete investigation. The System Monitor (sysmon.exe) utility from the Microsoft Sysinternals suite supplements the Security event log with this information.

Sysmon is not a replacement for SACL. The Security event records the authorization decision as Windows made it, which is the authoritative record. Sysmon records the process lineage around that decision. The two sources are complementary. Sysmon can capture the process that triggers an application pool recycle or an IIS restart when that activity matches the configured process creation rules.

Sample Sysmon Configuration for NDES

I have published a sample Sysmon configuration for NDES servers on GitHub. It is available here. This targeted configuration monitors SCEP enrollment settings, IIS application pool activity, registration authority certificates and private keys, credential access, and selected AD CS, certificate store, IIS, and operating-system security events relevant to an NDES deployment. It is not intended to serve as a general enterprise baseline. You can run it as a standalone configuration on an NDES server or merge its rule groups into an existing baseline such as SwiftOnSecurity or Olaf Hartong sysmon-modular.

Install or Update Sysmon

After reviewing and testing the sample configuration, install Sysmon or apply the configuration to an existing Sysmon deployment. To install Sysmon, open an elevated command window and run the following command.

sysmon.exe -accepteula -i sysmon-ndes.xml

To update an existing installation and confirm the active configuration, run the following commands.

sysmon.exe -c sysmon-ndes.xml
sysmon.exe -c

Sysmon events are written to the following event log location.

Applications and Services Logs > Microsoft > Windows > Sysmon > Operational.

Sysmon Monitoring Coverage

The configuration is centered on detecting changes to security-sensitive registry locations. The remaining rules capture the process, file, network, and system activity needed to investigate those changes.

  • Registry events 12, 13, and 14 cover the MSCEP key and its subkeys using a contains condition so both the native and WOW6432Node paths are captured. The same rule group covers the Certificate Services configuration key, machine certificate stores and trust anchors, SCHANNEL and TLS configuration, IIS service configuration, and common persistence locations.
  • Process creation events capture the tools used to make these changes, including reg.exe, regedit.exe, certutil.exe, appcmd.exe, and iisreset.exe. Tools such as appcmd.exe and iisreset.exe, as well as PowerShell commands such as Restart-WebAppPool, can recycle the SCEP application pool or restart IIS, allowing a modified template configuration to take effect. Sysmon can capture these actions when the initiating process and command line match the configured process-creation rules. PowerShell Script Block Logging provides additional visibility into commands executed within an existing PowerShell session, which might not be captured as separate Sysmon process-creation events.
  • Process access events are restricted to handle requests against LSASS with the access masks commonly associated with credential dumping. On a host that holds registration authority private keys and runs under a gMSA identity, this is the single highest-value rule.
  • File creation and deletion events cover the private key containers under Crypto\RSA, Crypto\Keys, and Crypto\PCPKSP, the IIS configuration directory, and certificate file extensions. Web content extensions such as .aspx and .ashx are also included, because a file with one of those extensions appearing under the NDES site is a strong indicator of a web shell.
  • Network connection events are purposefully scoped to scripting hosts and file transfer utilities rather than everything. An NDES server constantly talks to the CA and enrolling clients, and logging all of it produces excessive noise and provides little insight.
  • Named pipe, WMI, and process tampering events are also included. Legitimate volume for these is close to zero on a dedicated NDES server, so they are logged in full.

Note: The registry rule group includes an exclusion for the highest-volume legitimate writers, such as services.exe, svchost.exe, and TrustedInstaller.exe, as well as CreateKey events. Without those exclusions, useful events are buried within minutes. However, broad process-based exclusions can create visibility gaps. Administrators should validate that those exclusions do not suppress meaningful changes to the monitored paths.

Correlating Sysmon and Security Log Events

When a template value is modified, the Security event log records event ID 4657 with the account, value name, and old and new data. Sysmon records event ID 13 for the same operation, including the image path and process GUID associated with the write. If Sysmon also captured the corresponding process-creation event, the process GUID can be used to correlate event ID 13 with event ID 1, which provides the full command line and parent-process information. The Security event identifies the audited account and registry change, while Sysmon supplies richer process lineage and surrounding activity.

Sysmon event recording a change to the MSCEP registry key

Sysmon Deployment Considerations

Before deploying this configuration, confirm that it is compatible with your installed Sysmon version and existing monitoring baseline. Test it on a representative NDES server first, then review the resulting event volume and adjust only those rules that conflict with known, legitimate activity.

  • Confirm the schema version supported by your installed Sysmon build by running
    sysmon.exe -s before deployment. The sample configuration uses schema version 4.90 and therefore requires Sysmon 15 or later.
  • The FileBlockExecutable rules actively block matching executable files rather than merely recording their creation. Review and test these rules carefully before deploying the configuration in production.
  • If you already run a Sysmon baseline, merge these rule groups into it rather than installing this configuration over the top of it. Installing a new configuration replaces the existing one entirely.
  • Forward both the Sysmon Operational event log and the Security event log to your Security Information and Event Management (SIEM) platform. Local event logs on a compromised server hosting an Internet-published SCEP endpoint are of limited investigative value, and an alert that nobody receives provides little protection.
  • Validate the configuration in a lab first. Sysmon rule evaluation order is not always intuitive, and an overly broad include rule on a busy server can generate a surprising amount of data.

NDES Operational Considerations

Tightening permissions on a key that a running service depends on has consequences worth understanding before you make these changes in production.

  • If the Intune Certificate Connector service runs as a custom account rather than SYSTEM, add that account to the allowlist and grant it read access. Check the logon account on the connector service before you begin. In this scenario, I recommend running the Intune Certificate Connector as SYSTEM to follow security best practices.
  • If you change the SCEP application pool identity later, NDES will fail until the key permissions are updated to include the new identity. These permissions are aligned to a specific account, so any change to that account requires a corresponding change here.
  • If you operate multiple NDES servers, apply these changes to all of them to ensure full coverage.
  • Document any planned change to the template configuration in your change management process. If you alert on these events as recommended, someone will need to close the ticket the alert generates.

Validate Permissions, Auditing, and Enrollment

Run the following PowerShell command to verify the resulting access rules.

(Get-Acl -Path $Key).Access | Format-Table IdentityReference, RegistryRights, IsInherited

PowerShell output of the MSCEP registry key permissions

Only SYSTEM, BUILTIN\Administrators, and the NDES service account identity should be listed, and none of the entries should be inherited. Next, run the following command to confirm the audit rule is present.

(Get-Acl -Path $Key -Audit).Audit

PowerShell output of the MSCEP registry key audit settings

With the Registry audit subcategory enabled, perform a controlled test in a lab by changing a template value and recycling the SCEP application pool. Verify that event ID 4657 appears in the Security event log and includes the expected old and new values.

Security event ID 4657 recording a registry value change

Event ID 4657 details with the old and new registry values

If event ID 4657 does not appear in the Security event log after you modify a registry value under the MSCEP key, confirm that the Registry audit subcategory is enabled.

auditpol.exe /get /subcategory:"{0CCE921E-69AE-11D9-BED3-505054503030}"

auditpol output showing the Registry audit subcategory setting

If you deployed the Sysmon configuration, confirm it is loaded and generating events. Sysmon events are recorded in the Applications and Services Logs > Microsoft > Windows > Sysmon > Operational event log. The log should include events related to the test, including the registry value modification. Depending on how the application pool was recycled and which process-creation rules are enabled, it might also contain the process that initiated the recycle.

Sysmon event for the SCEP application pool recycle

You will also find events containing process creation details, including the full command line, process ID, parent process ID, and related information.

Sysmon process creation event details

Sysmon process creation event showing the command line and parent process

Finally, perform a device enrollment. Certificate enrollment should remain unaffected by these changes, and confirming successful enrollment after making these changes is essential before returning the NDES server to production.

Restore the Previous Registry Permissions

If enrollment breaks and the cause is not immediately obvious, administrators can restore the security descriptor captured earlier by running the following PowerShell commands.

$Sddl = Get-Content -Path '.\mscep-acl-backup.txt' -Raw
$Acl = Get-Acl -Path $Key -Audit
$Acl.SetSecurityDescriptorSddlForm($Sddl.Trim())
Set-Acl -Path $Key -AclObject $Acl
Restart-WebAppPool -Name SCEP

In practice, the most common cause of failure is granting access to the wrong service identity. This is why confirming the application pool identity is the first step in this process. Administrators should not skip this validation step.

Summary

The MSCEP registry values determine which certificate templates NDES uses for SCEP enrollment. An attacker with administrative access to the NDES server could modify these values and redirect enrollment to an unintended template without creating an obvious service failure. Restricting access to the MSCEP key reduces unnecessary exposure, while registry auditing and Sysmon provide visibility into unauthorized changes and the processes responsible for them. Forward these events to your SIEM, configure alerts for unexpected modifications, and validate the detection workflow before relying on it in production.

Additional Information

Sample Sysmon Configuration for NDES on GitHub

Download Sysmon from Microsoft Sysinternals

Microsoft NDES Information Disclosure: Detection and Remediation

Get-NdesNtlmDisclosure PowerShell Script on GitHub

Configure Infrastructure to Support SCEP with Microsoft Intune

Publish NDES with Microsoft Entra Application Proxy

Microsoft NDES Information Disclosure: Detection and Remediation

The Active Directory Certificate Services (AD CS) Network Device Enrollment Service (NDES) is a common building block for certificate deployment with Microsoft Intune. When NDES is used with the Intune Certificate Connector, the NDES server is published to the Internet so that managed devices can enroll for certificates using the Simple Certificate Enrollment Protocol (SCEP). Anything accessible via the Internet warrants careful attention, and a default NDES installation can expose more information than administrators might expect. Specifically, the NDES administration page can disclose sensitive information and should be removed. However, removing access to the page isn’t enough. Even after the NDES administration page is removed, IIS can continue to expose Windows authentication on that path through orphaned configuration entries, enabling NTLM-based information disclosure to unauthenticated Internet clients.

NDES Administration Page

NDES exposes its web-based functionality through applications hosted in Internet Information Services (IIS). A default NDES installation creates two IIS applications under the CertSrv virtual directory. The first is mscep, which is the SCEP endpoint itself. The second is mscep_admin, which is the NDES administration page used to generate one-time challenge passwords.

NDES IIS virtual directories mscep and mscep_admin

Standalone Configuration

In a standalone NDES deployment, an administrator browses to the administration page, authenticates, and receives a one-time password that is embedded in the certificate request. NDES validates the password against its local cache before forwarding the request to the certification authority (CA).

Intune Certificate Connector

The Intune Certificate Connector does not use this mechanism. Intune generates its own challenge, and the NDES policy module installed by the connector validates the certificate request against the authorizations Intune has granted for that specific device. The connector also disables the standard NDES challenge-password mechanism. As a result, the administration page is not used for Intune certificate enrollment and serves no operational purpose in this configuration.

Preauthentication Limitations

Reverse-proxy preauthentication normally provides an additional security boundary by requiring a client to authenticate before its request is forwarded to the published application. With Microsoft Entra Application Proxy, Microsoft Entra ID preauthentication can block unauthenticated Internet clients from reaching the internal web application, reducing its exposure to scanning, password-spraying attacks, and other malicious requests.

However, SCEP clients cannot complete the Microsoft Entra ID authentication process or present the authentication token required by Application Proxy. Enabling preauthentication would therefore prevent managed endpoints from reaching NDES and enrolling for certificates. When publishing NDES through Microsoft Entra Application Proxy, the Pre Authentication setting must be configured as Passthrough. Other reverse proxies must similarly allow unauthenticated requests to reach the NDES SCEP endpoint. The Intune SCEP policy module, not the reverse proxy, validates and authorizes certificate requests before NDES submits them to the certification authority.

NDES published through a reverse proxy with the Intune Certificate Connector

Information Disclosure

Because the NDES administration page is configured to use Windows authentication, IIS can negotiate NTLM with an unauthenticated client that requests the mscep_admin path. When the client sends an NTLM Type 1 negotiation message, IIS can respond with a Type 2 challenge containing a target information block. No credentials are required to receive this challenge or the information it contains.

Depending on the server configuration and negotiated NTLM options, the Type 2 message can disclose some or all of the following information:

  • NetBIOS domain name
  • NetBIOS computer name
  • DNS domain name
  • DNS computer name (server FQDN)
  • DNS forest name
  • Operating system version and build number

This is significant because the internal Active Directory namespace often differs from the public namespace used to publish NDES. For example, the published external name might be ndes1.example.com, but the NTLM challenge might disclose an internal domain of corp.example.net and a server FQDN of ndes1.corp.example.net. None of that information is available from public DNS.

Testing

To view disclosed information, use curl.exe to send a static NTLM type 1 negotiation message and inspect the response headers. Run the following command on the NDES server.

curl.exe -skI -H 'Authorization: NTLM TlRMTVNTUAABAAAAB4IIAAAAAAAAAAAAAAAAAAAAAAA=' https://localhost/certsrv/mscep_admin

curl output showing the NTLM challenge returned by the NDES administration page

A response containing a WWW-Authenticate header with an NTLM value indicates the endpoint is offering a challenge. When the header includes a base64 payload beginning with TlRMTVNTUAAC, the server has returned a type 2 message, leading to information disclosure on the target host. A response with no WWW-Authenticate header at all is the desired result.

Here are the PowerShell commands to decode and view the base64 payload and the disclosed information.

$Response = curl.exe -skI -H 'Authorization: NTLM TlRMTVNTUAABAAAAB4IIAAAAAAAAAAAAAAAAAAAAAAA=' https://localhost/certsrv/mscep_admin
$Challenge = ($Response | Select-String -Pattern '^WWW-Authenticate:\s*NTLM\s+(\S+)').Matches.Groups[1].Value
$Bytes = [Convert]::FromBase64String($Challenge)
[Text.Encoding]::Unicode.GetString($Bytes) -Replace '[^\x20-\x7E]', ' '

Decoded NTLM challenge revealing server and domain information

PowerShell Script

I have created and published a PowerShell script to simplify testing NDES servers for information leakage. The script is published on GitHub here. In addition, you can install the script from the PowerShell Gallery by running the following command.

Install-Script -Name Get-NdesNtlmDisclosure

The output of the command on a vulnerable server is as follows.

Get-NdesNtlmDisclosure output on a vulnerable NDES server

A Note on External Testing

When NDES is published using Microsoft Entra Application Proxy, external test results can be inconsistent. In my repeated testing across three separate servers, the first request sometimes returned a bare NTLM challenge with no payload, and a second request, seconds later, returned the full type 2 message. Therefore, do not treat a single clean external result as confirmation. Test locally on the server itself, then repeat external tests at least twice to confirm the results.

Additional Security Concerns

Beyond the namespace disclosure, an Internet-accessible NDES administration page introduces some additional security concerns.

Password Spray Attacks

An internet-facing endpoint that accepts domain credentials is a target for password spraying attacks. Failed authentication attempts can count toward the configured account-lockout threshold, introducing a potential denial-of-service risk to user accounts.

Service Disclosure

The presence of an NTLM challenge on the well-known mscep_admin path also identifies the server as an NDES host, providing attackers with useful reconnaissance information for targeted attacks.

Remediation

If the reverse proxy configuration exposes the NDES administration page, it becomes an Internet-accessible authenticated endpoint that no part of the Intune enrollment workflow uses. Removing it is a simple and effective attack-surface reduction measure.

Remove the NDES Administration Page

Administrators can remove the NDES administration page IIS application by opening an elevated PowerShell command window and running the following command.

Remove-WebApplication -Site 'Default Web Site' -Name 'CertSrv/mscep_admin'

After running this command, browsing to the administration page returns HTTP 404. The application is gone, and SCEP enrollment continues to work normally. At this point, most administrators would consider the task complete.

The Problem

If you attempt to browse the administration page after removing it, you will still be prompted to authenticate. If you supply valid credentials, you will receive the expected 404. The authentication prompt itself is the real problem.

IIS Authentication Settings

The NDES role configuration writes IIS authentication settings for the administration page into a separate location element in applicationHost.config. Remove-WebApplication deletes the application definition under the sites element, but it does not touch the location element. IIS continues to match the path and applies the authentication settings it finds there, even though no application exists at that location.

The orphaned configuration is visible only in the raw configuration file. It does not appear in IIS Manager, and Get-WebApplication reports nothing at that path. You can run the following command to view this configuration.

Select-String -Path "$env:SystemRoot\System32\inetsrv\config\applicationHost.config" -Pattern '<location path=|windowsAuthentication enabled='

On a server where the administration page has been removed, the output still includes an entry like the following.

991: <location path=”Default Web Site/CertSrv/mscep_admin”>
997: <windowsAuthentication enabled=”true”>

IIS configuration entries enabling Windows authentication for mscep_admin

Orphaned Configuration

To fully remediate this information disclosure and prevent potential disruption caused by password-spraying attacks, administrators must also remove the orphaned configuration location. This eliminates Windows authentication on the administration path entirely.

Remove-WebConfigurationLocation -Name 'Default Web Site/CertSrv/mscep_admin'

The NDES role configuration also enables Windows authentication on the SCEP application. Because anonymous authentication is also enabled, requests to the SCEP endpoint are ordinarily processed without an authentication challenge. However, Windows authentication is not required on an NDES server supporting the Intune Certificate Connector, and leaving it enabled on an Internet-facing endpoint serves no operational purpose. I recommend disabling it explicitly.

Set-WebConfigurationProperty -PSPath 'MACHINE/WEBROOT/APPHOST' -Location 'Default Web Site/CertSrv/mscep' -Filter 'system.webServer/security/authentication/windowsAuthentication' -Name 'enabled' -Value $False

By default, Windows authentication is handled in kernel mode, so stale state information in HTTP.SYS can continue serving challenges after a configuration change. Be sure to run iisreset.exe after making these changes.

iisreset.exe /restart

Validation

Repeat the NTLM probes demonstrated earlier against the administration page. The absence of a WWW-Authenticate header confirms the challenge has been eliminated.

curl.exe -skI -H 'Authorization: NTLM TlRMTVNTUAABAAAAB4IIAAAAAAAAAAAAAAAAAAAAAAA=' https://localhost/certsrv/mscep_admin

curl output confirming mscep_admin no longer offers an NTLM challenge

Confirm that SCEP still responds anonymously. A status code of 200 with a plain-text capability list is expected.

curl.exe -skI 'https://localhost/certsrv/mscep/mscep.dll?operation=GetCACaps'

curl output showing the SCEP GetCACaps response with status code 200

Repeat the first test against the internal FQDN and the published external FQDN, then perform a device enrollment to confirm the deployment is unaffected.

PowerShell

After making these changes, Get-NdesNtlmDisclosure.ps1 will indicate that the NTLM challenge is not offered.

Get-NdesNtlmDisclosure output showing the NTLM challenge is not offered

Summary

The NDES administration page is not required when the Intune Certificate Connector is installed on an NDES server and should be removed as part of attack surface reduction. However, removing the IIS application leaves behind an orphaned configuration location that continues to apply Windows authentication settings to the path. The result can be an Internet-facing endpoint that discloses the internal domain name, server name, forest name, operating system build, and other information to an unauthenticated remote client that initiates NTLM negotiation.

Removing the admin application alone is not enough. You must also remove the configuration location, disable Windows authentication where it is not required, restart IIS, and validate the result with an NTLM probe to ensure full mitigation.

Additional Information

Get-NdesNtlmDisclosure PowerShell Script on GitHub

NDES Security Hardening: Protecting and Monitoring the SCEP Registry Configuration

Publish NDES with Microsoft Entra Application Proxy

Intune Certificate Connector Configuration Failed

Microsoft Intune Certificate Connector Failure

Certificate Connector for Intune Configuration Failure

Create and Assign SCEP Profiles in Microsoft Intune

Simple Certificate Enrollment Protocol (RFC 8894)

Always On VPN Dynamic Profile Configurator (DPC) Webinar

Managing Microsoft Always On VPN deployments doesn’t have to be complicated. Always On VPN Dynamic Profile Configurator (DPC) is a free, open-source solution that simplifies deploying and managing Always On VPN client configuration settings while extending the capabilities of native deployment methods such as Microsoft Intune VPN profiles, custom XML, and PowerShell scripts.

Whether you’re deploying Always On VPN for the first time or looking to streamline an existing implementation, DPC provides powerful features that make advanced VPN configuration easier to deploy, manage, and maintain.

Join Our Live Webinar

Join me on Tuesday, August 18, at 1:00 PM EDT for a live webinar featuring Leo D’Arcy, creator and lead developer of Always On VPN DPC. During this live session, we’ll demonstrate many of DPC’s latest capabilities and show how they can simplify even the most complex Always On VPN deployments. Topics include:

  • Configuring flexible VPN deployment strategies
  • Optimizing interface and route metrics
  • Dynamically excluding Microsoft 365 and custom domains from the VPN tunnel
  • Configuring IKE mobility settings for improved roaming
  • Defining and managing IPv6 routes
  • Deploying DPC using Active Directory and Microsoft Intune
  • Exploring additional advanced features and deployment scenarios

Throughout the webinar, we’ll share practical guidance, configuration tips, and real-world examples to help you get the most from Always On VPN DPC.

Register Today

Registration is free, but you must register in advance to attend. If you can’t attend the live session, register anyway, and you’ll receive access to the on-demand recording after the event.

We’ll also reserve time for a live Q&A at the end of the presentation, giving you the opportunity to ask questions directly to the developers and experts behind DPC.

We look forward to seeing you there!

Additional Information

Always On VPN Dynamic Profile Configurator (DPC)

Always On VPN DPC Advanced Features

Always On VPN DPC with Microsoft Intune

Migrating from Always On VPN DPC Commercial to Open Source

Always On VPN DPC Commercial Support Now Available

Always On VPN DPC Discord Channel

The Myth of the Publish Certificate in Active Directory Setting

Certificate templates in Microsoft Active Directory Certificate Services (AD CS) provide powerful, preconfigured settings that enable administrators to issue certificates tailored for specific purposes. For example, a certificate template could allow a user to authenticate to a Wi-Fi network or VPN gateway. Another template might control policies for enrolling for web server certificates in the enterprise. Templates define settings such as cryptographic parameters (key algorithm and length), validity period, application policies, enrollment requirements, and more. While there are myriad settings to choose from, one in particular is often enabled unnecessarily. And while it works without issue, there can be some hidden downsides to enabling this setting.

Publish Certificate in Active Directory

When creating a certificate template, there’s an option on the General tab called Publish certificate in Active Directory. From experience, this is one of the most misunderstood settings for certificate templates.

Certificate template General tab with the Publish certificate in Active Directory option

Intuitively, it would make sense to check this box on all published certificate templates. After all, I want the users or devices targeted by this certificate template to find them in Active Directory (AD) so they can enroll. Many administrators believe that enabling this setting is required to ‘see’ the published certificate template on the endpoint, as shown here.

Active Directory Users and Computers Published Certificates tab listing certificates published for a user

However, enabling the Publish certificate in Active Directory option is not required for enrollment. To ‘see’ certificates available for enrollment, the user or device must only have the Enroll permission on the template.

What Is It For?

So, what does the Publish certificate in Active Directory setting do? When this option is enabled, the issuing CA adds the certificate to the requesting principal’s Active Directory account. There are two common scenarios where this is required.

S/MIME

Adding a user’s certificate to their AD account makes the public key centrally discoverable, allowing Outlook and other S/MIME-enabled clients to automatically find recipients’ certificates for secure email encryption and signature validation. Without the certificate published in AD, users must manually exchange certificates, breaking seamless S/MIME encryption in most enterprise environments.

Encrypting File System (EFS)

Publishing a user’s EFS certificate to their Active Directory account allows Windows to locate the correct public key automatically when encrypting files. It ensures recovery agents and key archival processes function properly. Without the certificate in AD, EFS can fail to encrypt data consistently across machines or prevent access to encrypted files when users roam or recover profiles.

Drawbacks

There are very few scenarios outside of S/MIME and EFS that require the Publish certificate in Active Directory option to be enabled. However, enabling it doesn’t necessarily break anything, and this setting is often enabled by default (or carried over from the source template when duplicating), so administrators may miss this option. Issuing certificates in this way introduces some potential problems.

AD Database Bloat

Adding a certificate to each principal’s AD object increases the size of each object, thereby increasing the total size of the AD database. For organizations with large directories with hundreds of thousands or even millions of accounts, adding unnecessary data to each account can be very expensive in terms of database size, replication traffic, backup storage, and overall domain performance. Making matters worse, certificates published to AD live perpetually. They are not removed automatically when certificates are revoked or expire.

Service Accounts

Service accounts used for certificate enrollment, such as the Microsoft Intune Certificate connector, can be especially challenging. Here, if the Publish certificate in Active Directory setting is enabled on the Intune certificate template, the CA will add a certificate to the service account for every certificate it issues. While you can have many certificates associated with a single account, there is an upper limit, approximately 1250, based on my testing. After that, certificates will continue to be issued, but adding them to AD will fail.

Remediation

The following recommendations can help administrators correct this misconfiguration and limit its impact in their environment.

Disable Unnecessary Certificate Publishing

Administrators should clear the Publish certificate in Active Directory setting on all certificate templates that do not explicitly require it, such as those used for S/MIME or Encrypting File System (EFS). This prevents new certificates from being written to user or computer objects and does not require certificates to be reissued.

Remove Published Certificates

Administrators can remove unnecessary certificates from user, computer, and service account objects in AD to reduce object and overall AD database sizes. Perform the following steps to remove unneeded certificates.

  1. Open the Active Directory Users and Computers management console (dsa.msc) and double-click the target principal.
  2. Select the Published Certificates tab.
  3. Select a certificate (or all certificates) and click Remove.
Removing published certificates from the Published Certificates tab in Active Directory Users and Computers

Important Note: Use extreme caution when deleting certificates! Do not delete any certificates unless you are certain they are not required.

Managed Service Accounts

Managed Service Accounts in AD do not have a Published Certificates tab. Administrators can use the Attribute Editor to remove individual certificates from the userCertificate attribute on the account.

Attribute Editor for a managed service account showing the userCertificate attribute

Managed Service Account Attribute Editor

userCertificate attribute entries for a managed service account

Managed Service Account userCertificate Entries

Unfortunately, there is no option to view the certificate in the UI for Managed Service Accounts. To view detailed certificate information, see the PowerShell section below.

Existing Certificates Are Not Removed Automatically

Disabling the Publish certificate in Active Directory setting only stops future certificates from being published in AD. Certificates already written to Active Directory are never removed automatically, even after they expire or are revoked. In environments where this setting has been enabled for an extended period, large numbers of stale certificates often accumulate and continue to increase the AD database size.

Intune Certificate Connector Considerations

This issue is especially problematic for high-volume enrollment scenarios that use service accounts, such as the Microsoft Intune Certificate Connector. When publishing is enabled for Intune certificate templates, certificates issued on behalf of users are added to the service account, quickly leading to excessive certificate accumulation and potential attribute limits.

ADPrincipalCertificate PowerShell Module

Manually performing this cleanup at scale is impractical. To assist administrators with cleaning up unnecessarily published certificates, I’ve created the ADPrincipalCertificate PowerShell module. This module includes functions to enumerate AD accounts that include certificates, show and optionally export certificates for AD accounts, and remove published certificates. The module also includes a function to enumerate published certificate templates that include the Publish certificate in Active Directory option enabled. You can install the ADPrincipalCertificate PowerShell module from the PowerShell gallery by running the following command.

Install-Module -Name ADPrincipalCertificate -Scope CurrentUser

See the ADPrincipalCertificate GitHub repository for detailed usage information.

Summary

While the Publish certificate in Active Directory option is helpful for S/MIME and EFS deployments, it is unnecessary for most other scenarios and is often enabled when it isn’t needed. This results in the unnecessary addition of certificates to AD accounts, causing individual objects and the entire AD database to grow without benefit. Sadly, many vendor guides indicate that this setting is required when it often isn’t, so many environments suffer from this misconfiguration. Administrators should review the certificate template configuration and disable this setting when it isn’t needed. Additionally, use the ADPrincipalCertificate PowerShell module to perform cleanup, if required.

Additional Information

Enterprise PKI with Microsoft Active Directory Certificate Services

ADPrincipalCertificate PowerShell Module on GitHub

ADPrincipalCertificate PowerShell Module in the PowerShell Gallery

Always On VPN SSTP Certificate Automation with CertKit

With public TLS certificates moving to significantly shorter certificate lifetimes, eventually just 47 days, Always On VPN administrators supporting Secure Socket Tunneling Protocol (SSTP) connections must prepare to address this eventuality. The first milestone for shortened public TLS certificate lifetimes is a few months, on March 15, 2026, when public TLS certificate lifetimes will be reduced from a maximum of 398 days to 200 days. One year later, on March 15, 2027, they will be reduced to just 100 days. Now is the time to begin planning an automated certificate enrollment solution to reduce administrative overhead and ensure uninterrupted connectivity for remote users.

Previous Approaches: PowerShell and Posh-ACME

In the past, I’ve written about using Let’s Encrypt Certificates for Always On VPN using the Posh-ACME PowerShell module. I’ve also posted some sample code to demonstrate how to integrate with a DNS provider to automate publishing the ACME challenge to public DNS for certificate enrollment verification. However, this assumes your DNS provider supports this option. Some do not. In addition, granting write permissions to public DNS via an API key introduces significant security risks. So, if direct ACME DNS automation isn’t viable in your environment, CertKit is an excellent alternative.

CertKit: Automated Certificate Issuance, Monitoring, and Alerting

To address these limitations, administrators can use the CertKit service. With CertKit, you delegate the Let’s Encrypt certificate enrollment process to them by simply creating a CNAME record in your public DNS. Once complete, CertKit handles the entire process transparently.

Issuance

Today, CertKit supports issuing certificates using Let’s Encrypt. In the future, support for additional certificate providers such as Google Trust and ZeroSSL will be added. CertKit supports Let’s Encrypt certificates using RSA (2048-bit) and Elliptic Curve (recommended). Certificates can be issued for an individual resource, a group of resources (multi-SAN), and a domain wildcard (e.g., *.example.net).

Monitoring

In addition to certificate issuance and management, CertKit offers domain TLS certificate monitoring to track your public assets. You can monitor your CertKit-managed certificates easily, but you can also add other services using TLS and track them on the same console. In this example, I’m using CertKit to manage certificates for two VPN servers (indicated by solid green dots) and to monitor my public websites, for which certificate management is handled by their respective hosting providers.

Alerts and Notifications

CertKit will automatically send emails to let you know when a certificate is expiring and if a CertKit-managed certificate has been renewed.

Pending certificate expiration.

Successful certificate renewal.

Certificate Retrieval

Once CertKit completes the enrollment process on your behalf, it stores all certificate files (.PFX, .PEM, and .KEY) in a secure S3-compatible storage bucket. While an administrator could easily retrieve and install them manually, automation will help reduce administrative overhead, especially as public TLS certificate lifetimes are further reduced. You can download certificate files programmatically in several ways.

PowerShell

The first way to download the certificate files from CertKit is to use the AWSPowerShell module. However, the AWSPowerShell is relatively heavy and is overkill for this specific use case. A better alternative is to use the MinIO client.

MinIO

The MinIO client (mc.exe) is an open-source command-line tool developed by MinIO. It provides access to any S3-compatible storage, which CertKit uses in its environment. MinIO is a single, portable executable installer that’s easy to use and well-documented.

Sample Code

I’ve published some sample code to demonstrate how to use the MinIO client to retrieve certificates from CertKit and install them on a Windows Server Routing and Remote Access (RRAS) server. You can find the sample code on GitHub here.

https://github.com/richardhicks/aovpn/blob/master/Install-SstpLetsEncryptCertificate-Certkit.ps1

Note: This sample code demonstrates how to download a .PFX file from CertKit and install it on the RRAS server. It is designed to run as a scheduled task in Windows during non-peak times. The code includes robust checks for service viability and will reboot the server if they fail. As such, this sample code could cause service disruptions, so use it with caution.

Cost

Today, CertKit is in beta and is free for everyone to use. In the future, there will be both free and paid tiers. You can learn more about their pricing models and sign up for the service at CertKit.io.

Learn More

If you’d like to learn more about CertKit and how you can leverage it for Always On VPN and other workloads in your environment, or you’d like to see a demonstration of CertKit, fill out the form below, and I’ll provide you with more information.

Additional Information

CertKit

Install-SstpLetsEncryptCertificate-Certkit.ps1 on GitHub

Always On VPN SSTP and 47-Day TLS Certificates

Always On VPN SSTP with Let’s Encrypt Certificates