Active Directory Certificate Services — LDAP enumeration of CA trust anchors and issued certificates with three-tier authentication
JIRA: TQR-1186
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.
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
# 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
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:
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.
CN=Certification Authorities)CN=Enrollment Services)CN=NTAuthCertificates)CN=AIA)Leaf certificates enrolled to domain objects — computers, users, and service accounts. These are the deployed certs that need PQC migration across the fleet.
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.
The scanner has two complementary ways to discover ADCS certificates. They can be run independently or together.
-scan-pki)Connect from anywhere to a DC via LDAP. Finds CA certs AND all issued domain certs.
config.encuser@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.
Operations Error 000004DC, it means the DC requires authentication before accepting search queries. Configure credentials with -config-adcs-user.
-scanfilesystem)Run directly on the CA server. Scans the local CertEnroll directory for CA certificate files. Windows only.
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.
| 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. |
CN=Public Key Services,CN=Services,CN=Configuration,DC=...userCertificate attributeStandard Domain Users have read access to AD PKI containers and userCertificate attributes by default.
Only attempted on non-Windows when a Kerberos credential cache (kinit) is available.
scanner@corp.comBare usernames like administrator are auto-converted to UPN using the discovered domain.
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.
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.
| LDAP Attribute | Purpose |
|---|---|
| cACertificate | DER-encoded CA certificate bytes |
| distinguishedName | Object DN for source path construction |
| cn | Common name of the CA or object |
| dNSHostName | Enrollment server hostname |
| msPKI-Enrollment-Servers | Enrollment Services URLs |
| userCertificate | Issued cert bytes on computer/user objects |
| sAMAccountName | Account name for source path labeling |
| objectClass | Filter: certificationAuthority objects |
Searched containers: CN=Certification Authorities, CN=NTAuthCertificates, CN=AIA, CN=Enrollment Services — all under CN=Public Key Services,CN=Services,CN=Configuration
unicodePwd or password attributesThe 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.
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.
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.
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).
/usr/bin/kinit user@CORP.EXAMPLE.COM first. The scanner reads the ticket from $KRB5CCNAME or /tmp/krb5cc_{uid}.AdcsUsername is set in config.enc, GSSAPI is bypassed and simple bind is tried directly.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
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.
000004DC indicates the DC accepted the anonymous connection but will not serve search results without credentials. Configure GSSAPI or simple bind to resolve this.
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.
| 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". |
| 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. |
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.
AdcsHost in config.enc)
Always wins. Set once: certscanner -config -config-adcs-host dc01.corp.com
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.
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.
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 |
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.
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.
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.
| Prefix | Origin | Example |
|---|---|---|
| ldap+pki:// | CA trust anchor from PKI container | ldap+pki://10.80.60.245/CN=Corp-Root,CN=Certification Authorities,...?ca_type=root |
| ldap+issued:// | Issued cert from domain object | ldap+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 |
| Field | Type | Description |
|---|---|---|
| event.action | keyword | pki_ca_discovered for CA trust anchors; pki_issued_cert_discovered for issued certs |
| event.dataset | keyword | Always pki_certificate — routes to tychon-pqc-certificates index |
| certificate.source_file_path | keyword | LDAP DN or filesystem path of origin. Prefix indicates cert type. |
| x509.public_key_algorithm | keyword | RSA | ECDSA | ML-DSA-65 | etc. |
| x509.public_key_size | integer | Key size in bits (RSA: 2048/4096, EC: 256/384/521) |
| x509.not_before | date | Certificate valid-from date (RFC 3339) |
| x509.not_after | date | Certificate expiration date (RFC 3339) |
| certificate.not_before | date | Alias of x509.not_before — consistent across all cert event types |
| certificate.not_after | date | Alias of x509.not_after — consistent across all cert event types |
| x509.subject.common_name | keyword | Certificate CN (CA name or enrolled entity name) |
| certificate.sha256_fingerprint | keyword | SHA-256 fingerprint used for deduplication and cross-scanner ID anchoring |
| tychon.pki.platform.type | keyword | Always adcs |
| tychon.pki.platform.host | keyword | DC hostname used for LDAP query (not the CA server hostname) |
| tychon.pki.platform.source_proto | keyword | Always ldap |
| tychon.pki.ca.name | keyword | CA common name extracted from the LDAP object or cert Subject |
| tychon.pki.ca.type | keyword | root | intermediate | issuing | ntauth (CA certs only) |
| tychon.pki.ca.pqc_vulnerable | boolean | True if RSA or ECDSA (classically-signed) |
| tychon.pki.ca.migration_priority | keyword | critical | high | medium | low |
DC discovered via DNS SRV. Auth via GSSAPI/Kerberos. No configuration needed.
certscanner.exe -scan-pki
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.
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!"
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
Production alternative to -insecure — provide the internal root CA cert for verification.
certscanner -scan-pki \
-adcs-cacert /path/to/internal-root-ca.pem
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.
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.
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>
Simple bind rejected by AD. Error code 52e = ERROR_LOGON_FAILURE. Check:
DOMAIN\user format — AD LDAP rejects this. Use UPN: user@domain.comnormalizing username when it auto-converts a bare username — verify the resulting domain is correctcertscanner -config -config-adcs-pass "new-pass"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.
The DC's TLS certificate is signed by an internal CA not in the scanner host's OS trust store.
-adcs-cacert /path/to/root-ca.pemcertlm.msc → Trusted Root CAs)-insecure skips verification and logs a warning/usr/bin/kinit user@CORP.EXAMPLE.COM first — uppercase realm required-config-adcs-user / -config-adcs-passauth: simple or auth: gssapi is required. If auth: anonymous, add credentials.userCertificate search on domain objects. Certs not enrolled to AD objects won't appear — use port scan for those.-adcs-basedn "DC=corp,DC=example,DC=com"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).