checker-ldap/README.md

119 lines
8 KiB
Markdown

# 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`).