Skip to main content

Microsoft AD CS

Issue from your Active Directory Certificate Services CA via the CES/CEP web services (MS-XCEP policy + MS-WSTEP enrollment). Templates and WS-Security username/password authentication.

CES/CEP vs WSTEP

AD CS exposes its web enrollment as two web-service endpoints, and CertAutoPilot uses both:

  • CEP (MS-XCEP — Certificate Enrollment Policy) returns the list of templates the caller may enrol. CertAutoPilot calls this on Sync.
  • CES (MS-WSTEPCertificate Enrollment Service) accepts the CSR and returns the issued certificate. "WSTEP" is simply the protocol this CES endpoint speaks — it is not a separate URL.

So there is no separate "WSTEP" field: you supply a CEP Endpoint URL (for template discovery) and a CES Endpoint URL (for enrolment). Both are required — CertAutoPilot cannot run CES-only without a CEP endpoint for templates.

Typical URLs — both endpoints must be the _UsernamePassword virtual directories, the only auth mode CertAutoPilot implements:

CEP: https://<ca-host>/ADPolicyProvider_CEP_<Auth>/service.svc
CES: https://<ca-host>/<CA-name>_CES_<Auth>/service.svc

Find the exact virtual-directory names in IIS Manager on the CA host (the *_CEP_* and *_CES_* applications under the enrollment site).

Prerequisites

  • An AD CS server with the CES/CEP (Certificate Enrollment Web Services) roles installed.
  • A service account in AD with permission to Read and Enroll the templates you want to use.
  • Network reachability from the worker to the enrollment endpoint (typically port 443 on the CA host or an IIS frontend).

Preflight check script

Run this from a Windows host on the same network path as the CertAutoPilot worker, before you add the provider. It prompts for your CEP URL, CES URL, and the AD service account, then for both endpoints checks: TCP reachability, that the TLS chain validates, and how the endpoint responds to an authenticated request. Green = ready; fix every FAIL first.

Two honest limits: (1) if the probe host is domain-joined it auto-trusts your enterprise root via group policy — the worker (often a Linux container) does not, so a TLS PASS here still requires pasting the issuing root into the profile's TLS-trust field unless the worker's CA bundle has it; (2) *_UsernamePassword vdirs run anonymous IIS authentication (credentials are verified inside the SOAP message, not by IIS), so this probe proves reachability but cannot verify the password. Neither can CertAutoPilot's own Test button: it treats a 401/403 as endpoint alive, auth enforced and still passes. Use Sync Templates to prove the credentials actually work.

# CertAutoPilot — Microsoft AD CS (CES/CEP) preflight. Run on the worker host.
$ErrorActionPreference = 'Continue'
$pass = 0; $fail = 0; $warn = 0
function Ok($m) { Write-Host "[ PASS ] $m" -ForegroundColor Green; $script:pass++ }
function Bad($m) { Write-Host "[ FAIL ] $m" -ForegroundColor Red; $script:fail++ }
function Warn($m) { Write-Host "[ WARN ] $m" -ForegroundColor Yellow; $script:warn++ }
function Info($m) { Write-Host " $m" -ForegroundColor Gray }

Write-Host "`n=== CertAutoPilot AD CS (CES/CEP) preflight ===`n" -ForegroundColor Cyan

$cepUrl = Read-Host "CEP Endpoint URL (.../ADPolicyProvider_CEP_<Auth>/service.svc)"
$cesUrl = Read-Host "CES Endpoint URL (.../<CA>_CES_<Auth>/service.svc)"
$cred = Get-Credential -Message "AD service account (DOMAIN\user) with Read + Enroll on your templates"

function Test-Endpoint($label, $url) {
Write-Host "`n--- $label : $url ---" -ForegroundColor Cyan
try { $u = [Uri]$url } catch { Bad "$label URL is not a valid URI"; return }
if ($u.Scheme -ne 'https') { Warn "$label is not HTTPS — CES/CEP should use TLS" }
if ($label -eq 'CEP' -and $url -notmatch '_CEP_') { Warn "CEP URL has no _CEP_ virtual dir — did you paste the CES URL?" }
if ($label -eq 'CES' -and $url -notmatch '_CES_') { Warn "CES URL has no _CES_ virtual dir — did you paste the CEP URL?" }

$port = if ($u.Port -gt 0) { $u.Port } else { 443 }

# 1. TCP reachability
$tcp = Test-NetConnection -ComputerName $u.Host -Port $port -WarningAction SilentlyContinue
if ($tcp.TcpTestSucceeded) { Ok "$label reachable at $($u.Host):$port" }
else { Bad "$label NOT reachable at $($u.Host):$port — DNS / routing / firewall"; return }

# 2. TLS: validate the chain + hostname the way a client would
$script:tlsOk = $true
try {
$client = [Net.Sockets.TcpClient]::new($u.Host, $port)
$cb = [Net.Security.RemoteCertificateValidationCallback]{
param($s, $cert, $chain, $errors)
if ($errors -ne [Net.Security.SslPolicyErrors]::None) { $script:tlsOk = $false }
return $true
}
$ssl = [Net.Security.SslStream]::new($client.GetStream(), $false, $cb)
$ssl.AuthenticateAsClient($u.Host)
$c = [Security.Cryptography.X509Certificates.X509Certificate2]$ssl.RemoteCertificate
Info "server cert: $($c.Subject) (expires $($c.NotAfter.ToString('yyyy-MM-dd')))"
if ($script:tlsOk) { Ok "$label TLS chain validates and hostname matches" }
else { Warn "$label TLS does NOT validate (self-signed / untrusted / name mismatch) — paste the issuing root as TLS trust, or make the worker trust it" }
if ($c.NotAfter -lt (Get-Date)) { Bad "$label server certificate is EXPIRED" }
$ssl.Dispose(); $client.Close()
} catch { Warn "$label TLS probe failed: $($_.Exception.Message)" }

# 3. Endpoint probe with the service account (NTLM / Negotiate on challenge).
# Kerberos/Windows-auth vdirs challenge with 401 — a non-401 there means the
# account was accepted. UsernamePassword vdirs are ANONYMOUS in IIS (the
# password is verified inside the SOAP message), so any 2xx/4xx only proves
# reachability — CertAutoPilot verifies the password itself on Save.
try {
$resp = Invoke-WebRequest -Uri $url -Credential $cred -UseBasicParsing -Method Get -TimeoutSec 20 -ErrorAction Stop
if ($url -match '_(CEP|CES)_UsernamePassword') {
Ok "$label reachable (HTTP $($resp.StatusCode)); UsernamePassword vdir is anonymous in IIS — password is verified by CertAutoPilot on Save"
} else {
Ok "$label authenticated OK (HTTP $($resp.StatusCode)) — the service account is accepted"
}
} catch {
$code = $_.Exception.Response.StatusCode.value__
if ($code -eq 401) { Bad "$label returned 401 — credentials rejected, or NTLM/Kerberos disabled on the endpoint's IIS auth settings" }
elseif ($code -eq 403) { Warn "$label returned 403 — authenticated but forbidden (account lacks rights on this vdir)" }
elseif ($code) { Ok "$label reachable (HTTP $code from a GET on a SOAP-only endpoint is expected)" }
elseif ($_.Exception.Message -match 'trust relationship|SSL|TLS') {
Warn "$label request blocked by TLS trust on THIS host: $($_.Exception.Message) (see the TLS check above — paste the issuing root into the profile's TLS trust field)"
}
else { Bad "$label request failed: $($_.Exception.Message)" }
}
}

Test-Endpoint 'CEP' $cepUrl
Test-Endpoint 'CES' $cesUrl

Write-Host "`n=== Result: $pass passed, $warn warning(s), $fail failed ===" -ForegroundColor Cyan
if ($fail) { Write-Host "Fix the FAIL items before adding the AD CS provider." -ForegroundColor Red }
elseif ($warn) { Write-Host "Reachable & authenticated — review the warnings (TLS trust / URL suffix) for your setup." -ForegroundColor Yellow }
else { Write-Host "All good — CEP and CES are reachable, TLS validates, and the account authenticates." -ForegroundColor Green }

Notes: the AD CS template Read/Enroll rights are checked at enrollment time, so also confirm those on the CA (see Prerequisites and Policy fetch returns 401). For Kerberos vdirs run the script as (or from) a domain-joined context — there the probe genuinely proves IIS accepted the account; the only credential check that actually exercises the account is Sync Templates; neither Save nor Test does.

Create the profile

  1. Settings → MSCA Connections → New. (Settings → CA Providers lists the seeded public ACME CAs and has no create action.)
  2. CEP Endpoint URL — the policy service (e.g. https://<ca-host>/ADPolicyProvider_CEP_UsernamePassword/service.svc).
  3. CES Endpoint URL — the enrollment service (e.g. https://<ca-host>/<CA-name>_CES_UsernamePassword/service.svc). Its _<Auth>_ suffix must match the CEP URL's and your chosen auth mode.
  4. Authentication — username and password. This is the only supported mode; the credentials travel inside the SOAP message as a WS-Security UsernameToken, not as HTTP NTLM or Negotiate.
  5. Optional: TLS trust — if your CA chain isn't system-trusted, paste the issuing root.
  6. Save — this performs no network call; it validates the fields and stores the encrypted credentials. Use Test for reachability and Sync Templates to fetch the template list.

Authentication

Username and password is the only implemented mode. Creating or updating a connection with any other auth type is rejected. Client-certificate (mutual-TLS) and Kerberos authentication to the CES/CEP endpoints are not supported.

The credentials are sent as a WS-Security UsernameToken inside the SOAP envelope; no HTTP Authorization header is ever set, so IIS never negotiates NTLM or Kerberos. Point both endpoints at the _UsernamePassword virtual directories, and pin a service account with the template permissions you need.

Templates

When issuing, the operator picks one of the templates returned by CEP. Common templates: WebServer, Computer, EnrollmentAgent. The template controls subject format, key usages, and validity period. CertAutoPilot does not evaluate those constraints locally — it only checks that the template name is in the synced list, so a mismatch surfaces as an MSCA_DENIED_BY_POLICY error from the CA at enrollment.

CertAutoPilot fetches the list via a live CEP GetPolicies call on every Sync, then stores it on the connection. Issuance validates the requested template against that stored copy, so a template added on the CA is only usable after a re-sync. If a newly added template doesn't appear after a re-sync, the cause is on the AD CS side — see New template doesn't appear after Sync.

Renewal

Renewal works the same as issuance — submit a new CSR against the same template. CertAutoPilot keeps the same logical certificate identity in the UI; the underlying X.509 changes, history is preserved.

Troubleshooting

Policy fetch returns 401

Either credentials are wrong or NTLM/Kerberos is disabled on the IIS authentication settings of the policy site. Test with curl --ntlm -u DOMAIN\user:pass <url> from the worker host.

New template doesn't appear after Sync

CertAutoPilot returns exactly what CEP GetPolicies gives it, so a missing template means the CA's policy endpoint isn't returning it. Duplicating a template in the Certificate Templates Console (certtmpl.msc) only creates the AD object — it is not offered for enrollment until all three of the following are true. Work through them in order, then re-Sync:

  1. Publish it to the CA. certsrv.msc → your CA → Certificate Templates → right-click → New → Certificate Template to Issue → select the template. (Duplicating ≠ publishing — this is the most common miss.)

  2. Grant Enroll to the service account. Template Properties → Security → add the account CertAutoPilot authenticates as → allow Enroll (or Autoenroll). CEP only returns templates the caller can enrol.

  3. Refresh the CEP policy cache. The policy web service caches its template set in memory, so a fresh template can lag even after steps 1–2. Recycle just the CEP/CES application pool on the CA's IIS frontend (least disruptive):

    Restart-WebAppPool -Name "<CEP/CES app pool>" # e.g. WSEnrollmentPolicyServer

    iisreset also works but bounces the whole IIS instance. Neither touches the CA engine, issued certificates, or CRL publishing — only the enrollment web front-end blips for a few seconds. If the domain has multiple DCs, also allow AD replication of the new template object (repadmin /syncall).

After steps 1–3, click Sync again — the new template appears. To see exactly what CEP returned, enable backend debug logging and look for the CEP GetPolicies raw response entry.

TLS handshake fails

The IIS frontend uses an internal CA your worker doesn't trust. Paste the issuing root into the profile's TLS trust field.

See also