Practical engineering guidance
Certificate auto-enrolment stopped working, and nothing told anybody
Auto-enrolment fails silently until certificates expire and authentication breaks. A read-only script that finds the certificates about to lapse, tests the certification authorities and reports who actually holds enrol rights.
Series: Active Directory
Certificate auto-enrolment is the quietest thing in a Windows estate. When it works, nobody notices. When it stops, nobody notices either — until certificates begin expiring and something that depends on them fails: 802.1X, VPN, LDAPS, domain controller authentication, a service that suddenly cannot present a client certificate.
The gap between “it stopped” and “somebody noticed” is usually the validity period of the certificate, which is to say a year.
The problem: three failures that look identical
From the client’s point of view, auto-enrolment can fail because:
- It cannot reach the certification authority. The CA is offline, DCOM is blocked, or the
RPC endpoint is unreachable. The documented symptom is “The RPC server is unavailable”,
error
0x800706ba, and event 82 from the certificate services client in the Application log. - It is not permitted to enrol. The template’s access control list does not grant the computer or user Read, Enroll and Autoenroll. Without Autoenroll specifically, manual enrolment works and automatic enrolment silently does nothing — which is the single most confusing version of this fault.
- The template is not offered. The CA is not configured to issue it, or the CA itself cannot read the template. Removing Authenticated Users from a template’s ACL breaks the CA’s own read access, and the CA then logs that the template could not be loaded.
All three present as “certificates are not renewing”. Only the first is obvious from the client.
Start with what is about to expire
The most useful question is not “why did auto-enrolment fail” but “what breaks, and when”. Enumerate the machine store, group by template, sort by expiry. That output tells you whether you have a week or a quarter, and it is the one piece of evidence that makes the problem legible to people who do not run certificate authorities.
The script
Read-only. It reads the local certificate store, discovers certification authorities from
Active Directory, tests reachability with certutil -ping, and reports who holds enrolment
rights on the templates you name. It enrols nothing and changes nothing.
<#
.SYNOPSIS
Reports certificate auto-enrolment health for a Windows computer.
.DESCRIPTION
Read-only. Reports machine certificates approaching expiry with their templates,
recent certificate-client errors, the enterprise certification authorities published
in Active Directory and whether each responds, and the enrolment rights held on
named templates. Enrols nothing and changes nothing.
Run elevated to read the machine store reliably. Template rights require the
ActiveDirectory module and read access to the configuration naming context.
.PARAMETER ExpiringDays
Report certificates expiring within this many days. Default 60.
.PARAMETER TemplateName
One or more template common names whose enrolment rights should be reported.
Use the template's CN, not its display name, when they differ.
.PARAMETER EventHours
How far back to read certificate client events. Default 48.
.PARAMETER ReportPath
Optional CSV path for the expiring-certificate list.
.EXAMPLE
.\Get-AutoEnrollmentHealth.ps1 -Verbose
.EXAMPLE
.\Get-AutoEnrollmentHealth.ps1 -TemplateName 'Machine','DomainController' -ExpiringDays 90
.NOTES
Read-only: this script makes no changes and does not trigger enrolment.
certutil -pulse would trigger a real enrolment attempt; this script does not call it.
Output contains certificate subjects and rights holders. Treat as internal information.
#>
#requires -Version 5.1
[CmdletBinding()]
param(
[ValidateRange(1, 730)]
[int]$ExpiringDays = 60,
[string[]]$TemplateName,
[ValidateRange(1, 720)]
[int]$EventHours = 48,
[ValidateScript({ Test-Path -Path (Split-Path -Path $_ -Parent) -PathType Container })]
[string]$ReportPath
)
$ErrorActionPreference = 'Stop'
$configurationNamingContext = $null
function Get-CertificateTemplateName {
<#
.SYNOPSIS
Reads the template name from a certificate's extensions, v1 or v2+.
#>
param([Parameter(Mandatory)]$Certificate)
foreach ($extension in $Certificate.Extensions) {
# v2 and later carry the template name in the Certificate Template Information
# extension; v1 carries only the template name as an OID-less string.
if ($extension.Oid.FriendlyName -match 'Certificate Template') {
$formatted = $extension.Format($false)
if ($formatted -match 'Template=([^,(]+)') { return $Matches[1].Trim() }
return $formatted.Trim()
}
}
return 'unknown'
}
# --- 1. What is about to expire ---------------------------------------------------------
$expiryCutoff = (Get-Date).AddDays($ExpiringDays)
$expiring = Get-ChildItem -Path Cert:\LocalMachine\My -ErrorAction Stop |
Where-Object { $_.NotAfter -le $expiryCutoff } |
ForEach-Object {
[pscustomobject]@{
Subject = $_.Subject
Template = Get-CertificateTemplateName -Certificate $_
NotAfter = $_.NotAfter.ToString('yyyy-MM-dd')
DaysRemaining = [math]::Floor(($_.NotAfter - (Get-Date)).TotalDays)
Thumbprint = $_.Thumbprint
HasPrivateKey = $_.HasPrivateKey
}
} | Sort-Object DaysRemaining
if ($expiring.Count -gt 0) {
Write-Warning "$($expiring.Count) machine certificate(s) expire within $ExpiringDays days."
}
if ($ReportPath) {
$expiring | Export-Csv -Path $ReportPath -NoTypeInformation -Encoding UTF8
Write-Verbose "Wrote $ReportPath"
}
# --- 2. Recent certificate client errors ------------------------------------------------
# Channel names for the certificate client have varied between Windows versions, so the
# available channels are discovered rather than assumed, and events are reported rather
# than interpreted by ID.
$since = (Get-Date).AddHours(-$EventHours)
$certEvents = [System.Collections.Generic.List[object]]::new()
$channels = @(Get-WinEvent -ListLog 'Microsoft-Windows-CertificateServicesClient*' -ErrorAction SilentlyContinue |
Where-Object { $_.RecordCount -gt 0 } | Select-Object -ExpandProperty LogName)
Write-Verbose "Certificate client channels present: $($channels -join ', ')"
foreach ($channel in $channels) {
try {
Get-WinEvent -FilterHashtable @{ LogName = $channel; Level = 1, 2, 3; StartTime = $since } -ErrorAction Stop |
ForEach-Object {
$certEvents.Add([pscustomobject]@{
TimeCreated = $_.TimeCreated
Source = $channel
Id = $_.Id
Level = $_.LevelDisplayName
Message = ($_.Message -split "`r?`n")[0]
})
}
}
catch { Write-Verbose "No qualifying events in $channel." }
}
# The enrolment engine also writes to the Application log under its own provider.
try {
Get-WinEvent -FilterHashtable @{
LogName = 'Application'
ProviderName = 'Microsoft-Windows-CertificateServicesClient-CertEnroll'
StartTime = $since
} -ErrorAction Stop | ForEach-Object {
$certEvents.Add([pscustomobject]@{
TimeCreated = $_.TimeCreated
Source = 'Application/CertEnroll'
Id = $_.Id
Level = $_.LevelDisplayName
Message = ($_.Message -split "`r?`n")[0]
})
}
}
catch { Write-Verbose 'No CertEnroll events in the Application log for this window.' }
# --- 3. Certification authorities published in Active Directory --------------------------
$authorities = [System.Collections.Generic.List[object]]::new()
try {
Import-Module ActiveDirectory -ErrorAction Stop
$configurationNamingContext = (Get-ADRootDSE -ErrorAction Stop).configurationNamingContext
$enrollmentServices = "CN=Enrollment Services,CN=Public Key Services,CN=Services,$configurationNamingContext"
Get-ADObject -SearchBase $enrollmentServices -Filter { objectClass -eq 'pKIEnrollmentService' } `
-Properties dNSHostName, cn, certificateTemplates -ErrorAction Stop |
ForEach-Object {
$configString = "$($_.dNSHostName)\$($_.cn)"
# certutil -ping tests the CA's Request interface. It is a read-only probe.
$null = & certutil.exe -ping -config $configString 2>&1
$reachable = ($LASTEXITCODE -eq 0)
if (-not $reachable) { Write-Warning "Certification authority did not respond: $configString" }
$authorities.Add([pscustomobject]@{
Name = $_.cn
Host = $_.dNSHostName
ConfigString = $configString
Reachable = $reachable
TemplateCount = @($_.certificateTemplates).Count
})
}
}
catch {
Write-Warning "Could not enumerate certification authorities from Active Directory: $($_.Exception.Message)"
}
# --- 4. Who holds enrolment rights on the named templates --------------------------------
$templateRights = [System.Collections.Generic.List[object]]::new()
if ($TemplateName -and $configurationNamingContext) {
# Extended right names are resolved from the schema rather than hard-coded, so the
# script reports whatever the directory itself calls them.
$extendedRights = @{}
Get-ADObject -SearchBase "CN=Extended-Rights,$configurationNamingContext" `
-Filter { objectClass -eq 'controlAccessRight' } -Properties displayName, rightsGuid -ErrorAction SilentlyContinue |
ForEach-Object { $extendedRights[$_.rightsGuid.ToLower()] = $_.displayName }
foreach ($name in $TemplateName) {
$templatePath = "CN=$name,CN=Certificate Templates,CN=Public Key Services,CN=Services,$configurationNamingContext"
try {
$acl = (Get-Acl -Path "AD:\$templatePath" -ErrorAction Stop).Access
foreach ($ace in $acl) {
if ($ace.ActiveDirectoryRights -notmatch 'ExtendedRight|GenericAll') { continue }
$rightName = if ($ace.ObjectType -and $extendedRights.ContainsKey($ace.ObjectType.ToString())) {
$extendedRights[$ace.ObjectType.ToString()]
}
elseif ($ace.ActiveDirectoryRights -match 'GenericAll') { 'Full control' }
else { 'all extended rights' }
if ($rightName -notmatch 'nroll') { continue }
$templateRights.Add([pscustomobject]@{
Template = $name
Identity = $ace.IdentityReference.Value
Right = $rightName
Type = $ace.AccessControlType
})
}
}
catch {
Write-Warning "Template '$name' rights unreadable: $($_.Exception.Message)"
}
}
}
[pscustomobject]@{
Device = $env:COMPUTERNAME
CollectedUtc = (Get-Date).ToUniversalTime().ToString('yyyy-MM-dd HH:mm')
ExpiringCertificates = $expiring
RecentCertificateEvents = $certEvents | Sort-Object TimeCreated -Descending
CertificationAuthorities = $authorities
TemplateEnrolmentRights = $templateRights
}
Interpreting what comes back
Certificates expiring and no recent events at all. Auto-enrolment is not running, or is running and finding nothing to do. Confirm the policy is applied: it is configured at Computer Configuration → Policies → Windows Settings → Security Settings → Public Key Policies → Certificate Services Client – Auto-Enrollment, with the configuration model enabled and both renewal options ticked. The equivalent user policy exists in the User Configuration branch, and a machine certificate problem needs the computer one.
A certification authority that does not respond. Start with the documented causes of
0x800706ba: the “Access this computer from the network” right, membership of the
Certificate Service DCOM Access group, DCOM enablement, and RPC restrictions. This is a
server-side or network fix; nothing on the client will help.
Enrolment rights that do not include an auto-enrolment right for the right principal. Computer certificates need the computer accounts — usually via Domain Computers or a group containing them — to hold Read, Enroll and Autoenroll. If only Read and Enroll are present, manual enrolment succeeds and automatic renewal never happens, which matches the symptom exactly.
Templates missing from the CA. TemplateCount of zero, or a template the CA does not
list, means the CA is not configured to issue it. Separately, if a template’s ACL has had
Authenticated Users removed, the CA may be unable to read the template at all — the CA logs
this, and the fix is to restore read access for the CA computer accounts.
The renewal trap worth knowing
If a template’s validity and renewal periods are misconfigured — the renewal period should be longer than eight hours and less than a fifth of the validity period — auto-enrolment will skip renewal and instead submit a brand-new request. On a template that requires manager approval, that request sits in the pending queue, and the certificate expires while a perfectly healthy auto-enrolment process waits for someone to approve something they did not expect.
Verification and limits
The 0x800706ba causes, the “no certificate templates could be found” causes, the requirement
for Read, Enroll and Autoenroll rights, the effect of removing Authenticated Users from a
template ACL, the Group Policy path, the certutil verbs used here (-ping) and the ones
deliberately avoided (-pulse, which triggers a real enrolment attempt), the Enrollment
Services container path, and the renewal-period trap were checked against current Microsoft
documentation on 20 September 2026.
The script was written for this article and statically analysed. It was not run against a production certification authority.
One limit is deliberate and worth being explicit about: I could not confirm on Microsoft Learn
the exact channel name or the event IDs of the certificate client’s auto-enrolment operational
log. Rather than publish event IDs I cannot source, the script discovers whichever certificate
client channels exist on the device and reports their errors and warnings as it finds them. If
you have a verified list of those IDs from your own environment, filtering on them will make
the output sharper. I have also not verified that a Get-CertificateAutoEnrollmentPolicy
cmdlet exists — the configuration cmdlet does, but reading the effective policy is done through
Group Policy results, not through a documented getter.
References
Reader feedback
Was this article useful?
No ratings yet. Be the first to rate this article.
