# checker-ldap LDAP directory checker for [happyDomain](https://www.happydomain.org/). Probes a domain's LDAP deployment end-to-end: SRV discovery (`_ldap._tcp`, `_ldaps._tcp`), transport security (StartTLS per RFC 2830, implicit TLS on port 636), RootDSE introspection (supportedSASLMechanisms, supportedControl, supportedLDAPVersion, namingContexts, vendor fingerprint), anonymous exposure (anonymous bind + baseObject search), plaintext-bind refusal posture, and -- when credentials are supplied -- an authenticated bind with an optional `baseObject` read on a base DN. TLS certificate chain / SAN / expiry / cipher posture is **out of scope** -- the dedicated TLS checker handles that. This checker only confirms that a TLS session can be established and records the negotiated TLS version and cipher for context. We publish each probed endpoint as a `DiscoveryEntry` of type `tls.endpoint.v1` so that `checker-tls` (or any other consumer of that contract) can run TLS posture checks against them without redoing the SRV lookup. For `_ldap._tcp` targets we emit `STARTTLS: "ldap"` with `RequireSTARTTLS: true`, so a misconfigured server that later drops StartTLS shows up as a CRIT, not a WARN. For `_ldaps._tcp` we emit direct-TLS endpoints (`STARTTLS: ""`). The TLS checker's resulting observations (under the `tls_probes` key) are folded back into our rule aggregation and HTML report via the SDK's `ObservationGetter.GetRelated` / `ReportContext.Related` path: a bad certificate on an LDAP endpoint shows up on the LDAP service page, not only in a separate TLS view. ## What it checks For each of `_ldap._tcp` (with fallback to port 389) and `_ldaps._tcp` (fallback to port 636): - **Reachability**: TCP connect on each resolved A/AAAA address, per IP family, timing captured. - **Transport security**: - On `_ldap._tcp`: whether the server advertises StartTLS in its RootDSE `supportedExtension` (OID 1.3.6.1.4.1.1466.20037), whether the StartTLS upgrade succeeds, and whether cleartext simple binds are refused with `confidentialityRequired` (resultCode 13) per RFC 4513 §5.1.2. - On `_ldaps._tcp`: whether the implicit TLS handshake succeeds. - **RootDSE introspection**: - `supportedLDAPVersion` -- flags a legacy LDAPv2 advertisement. - `supportedSASLMechanisms` -- warns when only PLAIN/LOGIN are offered and when no strong mechanism (SCRAM-*, EXTERNAL, GSSAPI) is present. - `supportedControl`, `supportedExtension`, `namingContexts`, `vendorName`, `vendorVersion` -- captured for the report. - **Anonymous exposure**: - Anonymous bind attempted; result noted. - When anonymous bind succeeds and at least one naming context is advertised, a `baseObject` search is issued on the first naming context. Any returned entry is flagged as `ldap.anon.search_allowed` -- the DIT is enumerable without credentials. - **Credential test (optional)**: when `bind_dn` and `bind_password` are supplied, a simple bind is performed **only on a TLS-protected channel**. When the bind succeeds and `base_dn` is supplied, a `baseObject` search is performed on that DN to confirm the account has read access to the intended subtree. ## Most common failure scenarios (addressed in the report) 1. **No encrypted endpoint reachable** → `ldap.no_encrypted_endpoint` / CRIT. Operator must enable either LDAPS or StartTLS. 2. **StartTLS not offered on 389** → `ldap.starttls.missing` / CRIT. Server-specific remediation included (OpenLDAP, 389-ds). 3. **StartTLS advertised but upgrade fails** → `ldap.starttls.handshake_failed` / CRIT. Hints to run the TLS checker for cipher/cert details. 4. **Cleartext bind accepted on 389 without StartTLS** → `ldap.plain_bind.accepted` / CRIT. Remediation via `olcSecurity` on OpenLDAP, `require_tls` on 389-ds. 5. **LDAPS handshake fails on 636** → `ldap.ldaps.handshake_failed` / CRIT. 6. **Anonymous search exposes DIT** → `ldap.anon.search_allowed` / WARN. 7. **Only PLAIN/LOGIN SASL offered** → `ldap.sasl.plain_only` / WARN. 8. **LDAPv2 still advertised** → `ldap.legacy_v2` / WARN. 9. **RootDSE unreadable on an otherwise working endpoint** → `ldap.rootdse.unreadable` / WARN. 10. **Provided bind DN / password fail** → `ldap.bind.failed` / CRIT -- surfaces credential / lockout issues immediately. ## Options | Id | Required | Description | |-----------------|----------|-----------------------------------------------------------------------------| | `domain` | yes | Auto-filled from the service scope (domain name). | | `timeout` | no | Per-endpoint timeout in seconds (default: 10). | | `bind_dn` | no | DN to bind as. Used only when `bind_password` is also set. | | `bind_password` | no | Secret. Bound only after TLS is established; never sent over cleartext. | | `base_dn` | no | Base DN to test read access against. Requires a successful authenticated bind. | ## Rules | Code | Description | Severity | |---------------------------------|-------------------------------------------------------------------------------------------------------------------|---------------------| | `ldap.has_srv` | Verifies that _ldap._tcp / _ldaps._tcp SRV records are published and resolvable. | WARNING | | `ldap.endpoint_reachable` | Verifies that every discovered LDAP endpoint accepts a TCP connection. | CRITICAL | | `ldap.has_encrypted_transport` | Verifies that at least one reachable endpoint offers an encrypted channel (LDAPS or StartTLS). | CRITICAL | | `ldap.starttls_supported` | Verifies that StartTLS is offered and succeeds on every reachable plain LDAP endpoint. | CRITICAL | | `ldap.ldaps_handshake` | Verifies that the direct TLS handshake succeeds on every LDAPS endpoint. | CRITICAL | | `ldap.starttls_on_ldaps` | Flags servers that needlessly advertise StartTLS on the implicit-TLS LDAPS port. | INFO | | `ldap.ipv6_reachable` | Verifies at least one endpoint is reachable over IPv6. | INFO | | `ldap.refuses_plain_bind` | Verifies the directory refuses authentication attempts over a cleartext channel. | CRITICAL | | `ldap.anonymous_search_blocked` | Flags directories that allow anonymous search of the naming context (information disclosure). | WARNING | | `ldap.rootdse_readable` | Verifies the RootDSE is readable over TLS and advertises naming contexts. | WARNING | | `ldap.sasl_mechanisms` | Reviews the supportedSASLMechanisms posture (presence of strong mechanisms, absence of password-equivalent ones). | WARNING | | `ldap.protocol_version` | Flags servers that still advertise the deprecated LDAPv2 protocol. | WARNING | | `ldap.bind_credentials` | Verifies the supplied bind credentials are accepted by the directory (only runs when bind_dn is set). | CRITICAL | | `ldap.base_dn_read` | Verifies the bound account can read the supplied base DN (only runs when base_dn is set and bind succeeded). | CRITICAL | | `ldap.tls_quality` | Folds the downstream TLS checker findings (certificate chain, hostname match, expiry) onto the LDAP service. | CRITICAL | ## License MIT (see `LICENSE` and `NOTICE`).