ADCS Certificate Discovery

Active Directory Certificate Services — LDAP enumeration of CA trust anchors and issued certificates with three-tier authentication

⚠ Critical: Target the Domain Controller, NOT the CA Server

This is the most common configuration mistake. ADCS runs on the CA server (the machine with certsrv, the CRL, and the private key). But the PKI certificate data lives in Active Directory, which is hosted on the Domain Controller (DC). These are often different machines.

🏛
CA Server (certsrv)
Runs: CertSvc, OCSP, web enrollment
Ports: 80/443 (web), no LDAP
≠
🗄
Domain Controller (DC)
Runs: Active Directory, LDAP
Ports: 389 (LDAP), 636 (LDAPS), 88 (Kerberos)

Why connecting to the CA server fails

The CA server is typically a domain member but does not expose LDAP ports 389/636 for client queries in the same way a DC does. When you connect LDAP to the CA server IP, you get connection refused on 389/636, or you connect but cannot search the CN=Public Key Services containers because they are served by the DC's global catalog, not the CA.

# Wrong — targeting the CA server
-config-adcs-host 10.80.60.247   # This is the certsrv machine → connection refused on 389/636

# Correct — targeting the DC
-config-adcs-host 10.80.60.245   # This is the domain controller → LDAP works

How to find your DC's IP

# Windows (run on any domain-joined machine)
nltest /dsgetdc:
# Output shows: DC: \\RnD-Lab-DC.ds.example.local  /  Address: \\10.80.60.245

# Linux
nslookup -type=SRV _ldap._tcp.dc._msdcs.your.domain.local

# macOS
dig SRV _ldap._tcp.dc._msdcs.your.domain.local

Overview

The ADCS connector discovers certificates by querying Active Directory via LDAP — targeting a Domain Controller, not the CA server itself. Because AD is replicated to every DC in the forest, a single DC query covers all CAs and all issued certificates across all domains in the forest.

Two certificate populations are returned:

1. CA Trust Anchors (PKI Containers)

Root, intermediate, and issuing CA certificates published to the four AD PKI containers. These define the PKI hierarchy and are the highest-priority targets for PQC migration.

  • ✓Root CA certificates (CN=Certification Authorities)
  • ✓Enterprise issuing CAs + enrollment URLs (CN=Enrollment Services)
  • ✓NTAuth (smart card / domain auth) CAs (CN=NTAuthCertificates)
  • ✓Intermediate / cross-cert CAs (CN=AIA)

2. Issued Domain Certificates (userCertificate)

Leaf certificates enrolled to domain objects — computers, users, and service accounts. These are the deployed certs that need PQC migration across the fleet.

  • ✓Domain controller authentication certificates
  • ✓Kerberos authentication certificates
  • ✓Directory email replication certificates
  • ✓Any auto-enrolled certificate on domain computers / users

What ADCS Discovery Does Not Find

  • —Certificates deployed to non-domain-joined hosts (use port scan for those)
  • —Revoked certificates in the CA's issued-cert database (only certs still published to AD objects are found)
  • —Certificate private keys (never leave the HSM or CA server)

Deduplication: SHA-256 fingerprints are deduplicated across all containers and all domain objects. A CA cert published in both CN=Certification Authorities and CN=NTAuthCertificates appears once in the output. An issued cert stored on multiple AD objects (rare) also appears once.

Remote Mode vs Local Mode

The scanner has two complementary ways to discover ADCS certificates. They can be run independently or together.

Remote Mode (-scan-pki)

Connect from anywhere to a DC via LDAP. Finds CA certs AND all issued domain certs.

Scanner Host
any machine, any OS
↓ TCP 389/636
Domain Controller
LDAP target — NOT the CA server
AD replication means one DC has data for all CAs in the forest

Authentication options (in priority order):

  1. GSSAPI/Kerberos — domain-joined host, no creds needed
  2. Simple bind — explicit credentials in config.enc
  3. Anonymous — fallback; often blocked by hardened AD
Username format: The scanner requires UPN format (user@domain.com) for simple bind. If you store a bare username like administrator, the scanner auto-converts it to administrator@your.domain.com using the domain discovered from RootDSE. Bare usernames and DOMAIN\user format are not accepted by AD LDAP simple bind.
Anonymous bind note: Many hardened AD deployments disable anonymous LDAP searches. When anonymous bind succeeds but container searches fail with Operations Error 000004DC, it means the DC requires authentication before accepting search queries. Configure credentials with -config-adcs-user.

Local Mode (-scanfilesystem)

Run directly on the CA server. Scans the local CertEnroll directory for CA certificate files. Windows only.

CA Server (certsrv)
scanner runs here
↓ local filesystem
C:\Windows\System32\CertSrv\CertEnroll\
*.crt — CA certificate files

Characteristics:

  • ✓No network access, no credentials needed
  • ✓Works in air-gapped environments
  • ✓Finds CA cert files the moment they are generated
  • —Only finds CA certs, not issued domain certs
  • —Windows only (path does not exist on Linux/macOS)
  • —Requires scanner to run on the CA server itself
Activation: The CertSrv\CertEnroll path is included automatically when you run -scanfilesystem on Windows. No additional flags needed. Certificates found here appear in the same output format as LDAP-discovered certs, with source_file_path pointing to the local file.

When to use which mode

Scenario Recommended Mode Notes
TYCHON agent deployed on domain-joined Windows endpoints -scan-pki (Remote) GSSAPI auto-auth. DC auto-discovered via DNS SRV. No config needed.
Non-domain-joined Linux/macOS scanner host -scan-pki (Remote) Configure credentials once with -config-adcs-*. Set host to DC IP/hostname, not CA server.
Air-gapped environment, no DC reachable -scanfilesystem on CA server (Local) Run scanner on the CA server itself. Finds CA cert files only.
Complete inventory — CA hierarchy + all deployed certs Both modes Run -scan-pki -scanfilesystem together. Duplication handled by SHA-256 dedup.
New ADCS installation, CA cert not yet replicated to AD -scanfilesystem on CA server (Local) AD replication can take minutes; local filesystem is always current.

Prerequisites

LDAP Access (Remote Mode)

  • • Reachable DC on TCP 636 (LDAPS) or 389 (StartTLS)
  • • Firewall allows scanner → DC on required port
  • • Read access to CN=Public Key Services,CN=Services,CN=Configuration,DC=...
  • • Read access to domain objects with userCertificate attribute

Standard Domain Users have read access to AD PKI containers and userCertificate attributes by default.

For Kerberos Auth (Recommended)

  • • Domain-joined Windows host, OR
  • • Kerberos KDC reachable on UDP/TCP 88
  • • Scanner running as a domain account
  • • No credentials to configure — GSSAPI auto-negotiates

Only attempted on non-Windows when a Kerberos credential cache (kinit) is available.

For Simple Bind (Non-Domain Host)

  • • Any host — domain-joined not required
  • • LDAPS (port 636) or StartTLS required — plain LDAP rejected
  • • Standard read-only domain user account
  • • Username in UPN format: scanner@corp.com

Bare usernames like administrator are auto-converted to UPN using the discovered domain.

AD Service Account — Least Privilege

Create a dedicated read-only domain user account for scanning. No group memberships beyond standard Domain Users are required. Do not use Domain Admin. The service account should have a non-expiring password or an automated rotation mechanism to prevent scan failures at renewal time.

Minimum AD Permissions

The scanner performs read-only LDAP searches — it never writes, modifies, creates, or deletes any Active Directory object or attribute. Standard Domain Users membership is sufficient; no elevated groups are needed.

What the Scanner Reads

LDAP AttributePurpose
cACertificateDER-encoded CA certificate bytes
distinguishedNameObject DN for source path construction
cnCommon name of the CA or object
dNSHostNameEnrollment server hostname
msPKI-Enrollment-ServersEnrollment Services URLs
userCertificateIssued cert bytes on computer/user objects
sAMAccountNameAccount name for source path labeling
objectClassFilter: certificationAuthority objects

Searched containers: CN=Certification Authorities, CN=NTAuthCertificates, CN=AIA, CN=Enrollment Services — all under CN=Public Key Services,CN=Services,CN=Configuration

What the Scanner Never Does

  • ✗No LDAP writes, modify, add, or delete operations
  • ✗No access to unicodePwd or password attributes
  • ✗No access to private keys (never stored in AD)
  • ✗No schema modifications
  • ✗No Group Policy or GPO access
  • ✗No certificate enrollment or revocation
  • ✗No access to Certificate Templates (write attributes)

Groups to Explicitly Exclude

The service account must not be a member of any of the following. These groups grant write or control-plane access that scanning does not require and that expands the blast radius if the account is compromised.

• Domain Admins
• Enterprise Admins
• Schema Admins
• Cert Publishers
• Account Operators
• Backup Operators
• Server Operators
• Group Policy Creator Owners

Create a Dedicated Service Account (PowerShell)

Run on a domain-joined Windows host with the ActiveDirectory module loaded (RSAT tools). Replace the password placeholder before running.

# Create a read-only domain user for TYCHON PKI scanning
$securePass = ConvertTo-SecureString "ReplaceWithStrongPassword!" -AsPlainText -Force

New-ADUser `
    -Name            "svc-tychon-pki" `
    -SamAccountName  "svc-tychon-pki" `
    -UserPrincipalName "svc-tychon-pki@corp.example.com" `
    -Description     "TYCHON PKI scanner — read-only LDAP, no interactive logon" `
    -AccountPassword $securePass `
    -PasswordNeverExpires $true `
    -CannotChangePassword $true `
    -Enabled         $true

# Verify: should only show Domain Users
Get-ADUser "svc-tychon-pki" -Properties MemberOf |
    Select-Object -ExpandProperty MemberOf

Optionally deny interactive logon via a GPO or local security policy (Defense in Depth). These restrictions do not affect LDAP authentication.

# Optional: deny interactive and RDP logon (apply via GPO "User Rights Assignment")
# "Deny log on locally"                   → add svc-tychon-pki
# "Deny log on through Remote Desktop Services" → add svc-tychon-pki
# "Deny log on as a batch job"             → add svc-tychon-pki (if not needed for scheduled scans)

# Confirm no elevation occurred
Get-ADUser "svc-tychon-pki" -Properties MemberOf | Select-Object Name, MemberOf

Default AD permissions are sufficient. Microsoft grants all Domain Users read access to the CN=Public Key Services container and the userCertificate attribute by default. No additional ACL changes are needed in a standard AD deployment. If your environment has hardened ACLs on the PKI containers, see the Troubleshooting section for how to diagnose and restore default read access.

Authentication — Three-Tier Strategy

The ADCS connector tries three authentication methods in sequence. The first that succeeds is used for the full scan session. The RootDSE query (which discovers the domain base DN) always runs before authentication, so the scanner can construct a proper UPN even when only a bare username is stored.

1

GSSAPI / Kerberos (Preferred)

Recommended for domain-joined hosts

The scanner presents a Kerberos service ticket for the DC obtained via GSSAPI. No username or password configuration is required — the running process credential (Windows LSASS / kinit ticket cache on Linux/macOS) is used automatically. LDAP channel binding tokens are included to satisfy the March 2020 Microsoft security hardening requirement (KB4520412).

  • • Windows: Runs as a domain account (SYSTEM or service account joined to domain). Kerberos tickets are acquired automatically.
  • • Linux/macOS: Run /usr/bin/kinit user@CORP.EXAMPLE.COM first. The scanner reads the ticket from $KRB5CCNAME or /tmp/krb5cc_{uid}.
  • • When skipped: If AdcsUsername is set in config.enc, GSSAPI is bypassed and simple bind is tried directly.
2

Simple Bind over TLS

For non-domain-joined hosts

Username and password sent after TLS is established. The scanner requires LDAPS or StartTLS before transmitting credentials — plain-text credential send is never permitted. Credentials are stored in config.enc and never appear in process arguments or scan logs.

Username auto-normalization: AD LDAP simple bind requires UPN format (user@domain.com) or DN format (CN=user,OU=...,DC=corp,DC=com). The scanner discovers the domain from RootDSE before attempting bind, so a stored bare username like administrator is automatically converted to administrator@corp.example.com. A normalization notice is logged when this conversion occurs.

Formats accepted: user@domain.com (UPN — preferred), CN=user,DC=corp,DC=com (DN), bare username (auto-converted to UPN)

Formats NOT accepted by AD: DOMAIN\user (NetBIOS/NTLM style) — AD LDAP simple bind does not accept this format

3

Anonymous / Unauthenticated Bind

Fallback Only

Attempts UnauthenticatedBind("") as a last resort. A warning is always logged. In default AD configurations, anonymous bind succeeds for RootDSE but fails for PKI container searches — the DC returns Operations Error 000004DC: a successful bind must be completed. This means anonymous results in zero discovered certificates on most enterprise AD deployments.

Why anonymous fails on hardened AD: Microsoft's default since Windows Server 2008 is to require at least an unauthenticated bind to succeed, but then restrict actual searches to authenticated sessions. The error 000004DC indicates the DC accepted the anonymous connection but will not serve search results without credentials. Configure GSSAPI or simple bind to resolve this.

Configuration

Security model: ADCS credentials are written once to config.enc (AES-256-GCM, PBKDF2-SHA256, 600k rounds) using -config subcommand flags. They are never passed as runtime scan flags and never appear in process argument lists or logs.

ADCS Configuration Parameters (stored in config.enc)

Config Flag Key in config.enc Default Description
-config-adcs-host AdcsHost Auto-detected DC hostname or IP — not the CA server. Auto-detected via DNS SRV on domain-joined hosts. Use nltest /dsgetdc: to find the DC.
-config-adcs-port AdcsPort 636 LDAP port on the DC. 636 = LDAPS. Use 389 with -config-adcs-tls starttls.
-config-adcs-tls AdcsTLSMode ldaps ldaps — TLS from connect (port 636). starttls — upgrade plain connection to TLS (port 389). none — anonymous only, credentials refused.
-config-adcs-user AdcsUsername — (GSSAPI tried first) Bind username. UPN format preferred: scanner@corp.com. Bare usernames auto-converted to UPN using discovered domain. If empty, GSSAPI is attempted.
-config-adcs-pass AdcsPassword — Bind password. Stored encrypted. Refused when AdcsTLSMode == "none".

Runtime Scan-time Overrides (not stored in config.enc)

Flag Description
-adcs-cacert <path> PEM CA cert for LDAP TLS verification. Use when the DC's certificate is signed by an internal CA not in the OS trust store. Not stored in config.enc.
-adcs-basedn <dn> Override auto-discovered base DN (normally from RootDSE). Use when RootDSE query is blocked by AD policy. Example: DC=corp,DC=example,DC=com
-insecure Skip TLS certificate verification for LDAP connections. Logs a warning. For lab use only — do not use in production.

DC Auto-Detection

On domain-joined Windows hosts, -scan-pki can run with zero configuration. The scanner locates the DC and authenticates automatically. Auto-detection always finds a domain controller, never the CA server.

DC Discovery Priority Chain

  1. 1
    Explicit config (AdcsHost in config.enc)

    Always wins. Set once: certscanner -config -config-adcs-host dc01.corp.com

  2. 2
    USERDNSDOMAIN env var + DNS SRV lookup

    Queries _ldap._tcp.dc._msdcs.{USERDNSDOMAIN}. On domain-joined Windows, USERDNSDOMAIN is always set by the system. Uses highest-priority DC from SRV response.

  3. 3
    No host found — scan skipped

    A warning is logged and the PKI scan is skipped without aborting the parent scan. Set AdcsHost explicitly to resolve.

Base DN auto-discovery: After connecting to the DC, the scanner queries RootDSE (base object at empty DN) and reads the configurationNamingContext attribute. This gives the exact path for CN=Public Key Services,CN=Services,{configNC} without manual input. This query runs before authentication, so the discovered domain can also be used to normalize usernames. Override with -adcs-basedn if RootDSE is blocked.

What Gets Searched in AD

CA Trust Anchor Containers

All four containers are under CN=Public Key Services,CN=Services,{configurationNamingContext}. They are replicated to every DC in the forest, so a single DC query covers all CAs in all domains.

Container CN CA Type Key LDAP Attribute tychon.pki.ca.type
CN=Certification Authorities Root and standalone CAs trusted by the forest cACertificate (DER) root
CN=NTAuthCertificates CAs trusted for smart card / domain auth (NTAuth store) cACertificate (multi-value) ntauth
CN=AIA Intermediate, cross-certification, and subordinate CAs cACertificate (multi-value) intermediate
CN=Enrollment Services Enterprise issuing CAs (CA name, enrollment URL, templates) cACertificate + dNSHostName + certificateTemplates issuing

Issued Certificate Search

After the PKI containers, the scanner performs a second paged search across the entire domain base DN for AD objects with a userCertificate attribute. This finds all certificates enrolled to domain members.

# LDAP filter used for issued cert search
(&(objectClass=*)(userCertificate=*))

# Attributes retrieved
userCertificate, distinguishedName, cn, sAMAccountName, dNSHostName, objectClass

This matches any AD object type — computer accounts (objectClass=computer), user accounts (objectClass=user), and managed service accounts that have had certificates enrolled via ADCS auto-enrollment or manual enrollment.

Paged Search (RFC 2696)

Both searches use RFC 2696 paging with page size 500. Without paging, Active Directory truncates results at 1,000 objects. For domains with large numbers of enrolled computers or cross-certificates, this would silently drop results. Every page is fetched until the server returns an empty paging cookie.

Output Fields

Each discovered certificate produces one event in NDJSON output. The certificate.source_file_path field indicates where the cert came from and what type it is.

Source Path Prefixes

Prefix Origin Example
ldap+pki://CA trust anchor from PKI containerldap+pki://10.80.60.245/CN=Corp-Root,CN=Certification Authorities,...?ca_type=root
ldap+issued://Issued cert from domain objectldap+issued://10.80.60.245/CN=RND-LAB-DC,OU=Domain Controllers,DC=ds,...
C:\Windows\System32\CertSrv\...Local filesystem (local mode)C:\Windows\System32\CertSrv\CertEnroll\rnd-lab-adcs.crt

Key Output Fields

Field Type Description
event.actionkeywordpki_ca_discovered for CA trust anchors; pki_issued_cert_discovered for issued certs
event.datasetkeywordAlways pki_certificate — routes to tychon-pqc-certificates index
certificate.source_file_pathkeywordLDAP DN or filesystem path of origin. Prefix indicates cert type.
x509.public_key_algorithmkeywordRSA | ECDSA | ML-DSA-65 | etc.
x509.public_key_sizeintegerKey size in bits (RSA: 2048/4096, EC: 256/384/521)
x509.not_beforedateCertificate valid-from date (RFC 3339)
x509.not_afterdateCertificate expiration date (RFC 3339)
certificate.not_beforedateAlias of x509.not_before — consistent across all cert event types
certificate.not_afterdateAlias of x509.not_after — consistent across all cert event types
x509.subject.common_namekeywordCertificate CN (CA name or enrolled entity name)
certificate.sha256_fingerprintkeywordSHA-256 fingerprint used for deduplication and cross-scanner ID anchoring
tychon.pki.platform.typekeywordAlways adcs
tychon.pki.platform.hostkeywordDC hostname used for LDAP query (not the CA server hostname)
tychon.pki.platform.source_protokeywordAlways ldap
tychon.pki.ca.namekeywordCA common name extracted from the LDAP object or cert Subject
tychon.pki.ca.typekeywordroot | intermediate | issuing | ntauth (CA certs only)
tychon.pki.ca.pqc_vulnerablebooleanTrue if RSA or ECDSA (classically-signed)
tychon.pki.ca.migration_prioritykeywordcritical | high | medium | low

Usage Examples

Domain-joined Windows — zero config, GSSAPI auto-auth

DC discovered via DNS SRV. Auth via GSSAPI/Kerberos. No configuration needed.

certscanner.exe -scan-pki

Non-domain-joined host — configure DC + credentials, then scan

Target the DC (not the CA server). Credentials stored encrypted in config.enc.

# Step 1: one-time setup — store DC address and credentials
# Use the DC's IP or hostname, NOT the CA server's address
certscanner -config \
  -config-adcs-host 10.80.60.245 \
  -config-adcs-port 389 \
  -config-adcs-tls starttls \
  -config-adcs-user scanner@corp.example.com \
  -config-adcs-pass "Str0ng-P@ssword!"

# Step 2: run scans — no credentials in scan command
certscanner -scan-pki

The scanner logs normalizing username "scanner" → "scanner@corp.example.com" if you stored a bare username.

LDAPS on port 636

certscanner -config \
  -config-adcs-host dc01.corp.example.com \
  -config-adcs-port 636 \
  -config-adcs-tls ldaps \
  -config-adcs-user scanner@corp.example.com \
  -config-adcs-pass "Str0ng-P@ssword!"

Lab environment with self-signed DC cert

Use -insecure when the DC's TLS cert is self-signed or issued by an internal CA not in the OS trust store.

certscanner -scan-pki -insecure

Internal CA cert for TLS verification

Production alternative to -insecure — provide the internal root CA cert for verification.

certscanner -scan-pki \
  -adcs-cacert /path/to/internal-root-ca.pem

Local mode — run on the CA server itself (Windows only)

Scans C:\Windows\System32\CertSrv\CertEnroll\ without any network connectivity needed.

certscanner.exe -scanfilesystem

No LDAP connection. Finds CA cert files only — not issued domain certs.

Complete inventory — both modes together

Maximum coverage: CA certs + issued domain certs via LDAP, plus local CA cert files if running on the CA server.

certscanner.exe \
  -scanfilesystem \
  -scan-pki \
  -outputformat flatndjson \
  -output C:\ProgramData\Tychon\scan-results.ndjson

SHA-256 deduplication prevents the same cert appearing twice if found by both modes.

Troubleshooting

Connection refused on port 389/636 — most common mistake

You are connecting to the CA server instead of the Domain Controller. The CA server does not serve LDAP on 389/636 the same way a DC does.

To find the DC address:

# Windows
nltest /dsgetdc:
# Shows: DC: \\dc01.corp.com  /  Address: \\10.x.x.x

# Linux / macOS
nslookup -type=SRV _ldap._tcp.dc._msdcs.your.domain.local

Then update: certscanner -config -config-adcs-host <DC-IP-or-hostname>

LDAP Result Code 49 "Invalid Credentials" (data 52e)

Simple bind rejected by AD. Error code 52e = ERROR_LOGON_FAILURE. Check:

  • Username must not be DOMAIN\user format — AD LDAP rejects this. Use UPN: user@domain.com
  • The scanner logs normalizing username when it auto-converts a bare username — verify the resulting domain is correct
  • Account is not locked, expired, or disabled
  • Password is correct — update: certscanner -config -config-adcs-pass "new-pass"
  • Account does not require smart card authentication

Operations Error 000004DC — anonymous bind disabled

The anonymous bind succeeded but the DC refused to serve search results. This is normal for hardened AD deployments. The log shows:

ldap_cert_scanner: using anonymous bind — ...
ldap_cert_scanner: search in CN=Certification Authorities,...: LDAP Result Code 1 "Operations Error": 000004DC: ...
comment: In order to perform this operation a successful bind must be completed on the connection.

Configure explicit credentials with -config-adcs-user and -config-adcs-pass to resolve this.

TLS: x509: certificate signed by unknown authority

The DC's TLS certificate is signed by an internal CA not in the scanner host's OS trust store.

  • Export internal root CA cert as PEM; pass -adcs-cacert /path/to/root-ca.pem
  • Import internal CA cert into OS trust store (Windows: certlm.msc → Trusted Root CAs)
  • Lab only: -insecure skips verification and logs a warning

GSSAPI fails — "no Kerberos ticket" or KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN

  • Windows: ensure scanner runs as a domain account (not Local System or a local administrator)
  • Linux/macOS: run /usr/bin/kinit user@CORP.EXAMPLE.COM first — uppercase realm required
  • Fallback: configure simple bind credentials with -config-adcs-user / -config-adcs-pass

Found fewer certificates than expected

  • Confirm auth tier in log — auth: simple or auth: gssapi is required. If auth: anonymous, add credentials.
  • Issued certs (e.g. from certsrv MMC "Issued Certificates") are found via the userCertificate search on domain objects. Certs not enrolled to AD objects won't appear — use port scan for those.
  • RootDSE blocked? Supply -adcs-basedn "DC=corp,DC=example,DC=com"

Error: credentials rejected when TLS mode is none

By design — the scanner refuses to transmit credentials over an unencrypted LDAP connection. Either set -config-adcs-tls ldaps or starttls, or remove the stored credentials to use anonymous bind (not recommended for production).