279 lines
9.2 KiB
Go
279 lines
9.2 KiB
Go
package checker
|
|
|
|
import (
|
|
"context"
|
|
|
|
sdk "git.happydns.org/checker-sdk-go/checker"
|
|
)
|
|
|
|
// Rules in this file cover transport-level concerns: SRV discovery, TCP
|
|
// reachability, encryption availability, TLS handshakes. They read raw
|
|
// LDAPData fields directly (no pre-derived Issues slice).
|
|
|
|
// srvDiscoveryRule: _ldap._tcp / _ldaps._tcp SRV publishing + resolution.
|
|
type srvDiscoveryRule struct{}
|
|
|
|
func (r *srvDiscoveryRule) Name() string { return "ldap.has_srv" }
|
|
func (r *srvDiscoveryRule) Description() string {
|
|
return "Verifies that _ldap._tcp / _ldaps._tcp SRV records are published and resolvable."
|
|
}
|
|
|
|
func (r *srvDiscoveryRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
|
|
data, errSt := loadLDAPData(ctx, obs)
|
|
if errSt != nil {
|
|
return []sdk.CheckState{*errSt}
|
|
}
|
|
var states []sdk.CheckState
|
|
if data.SRV.FallbackProbed {
|
|
states = append(states, infoState(
|
|
CodeNoSRV,
|
|
"No LDAP SRV records published for "+data.Domain+".",
|
|
"",
|
|
"Consider publishing _ldap._tcp."+data.Domain+" and _ldaps._tcp."+data.Domain+" SRV records to let clients discover the directory automatically.",
|
|
))
|
|
}
|
|
for prefix, msg := range data.SRV.Errors {
|
|
states = append(states, warnState(
|
|
CodeSRVServfail,
|
|
"DNS lookup failed for "+prefix+data.Domain+": "+msg,
|
|
"",
|
|
"Check the authoritative DNS servers for this domain.",
|
|
))
|
|
}
|
|
if len(states) == 0 {
|
|
return []sdk.CheckState{passState("ldap.has_srv.ok", "SRV records are published and resolved cleanly.")}
|
|
}
|
|
return states
|
|
}
|
|
|
|
// endpointReachableRule: every discovered endpoint accepts a TCP connection.
|
|
type endpointReachableRule struct{}
|
|
|
|
func (r *endpointReachableRule) Name() string { return "ldap.endpoint_reachable" }
|
|
func (r *endpointReachableRule) Description() string {
|
|
return "Verifies that every discovered LDAP endpoint accepts a TCP connection."
|
|
}
|
|
|
|
func (r *endpointReachableRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
|
|
data, errSt := loadLDAPData(ctx, obs)
|
|
if errSt != nil {
|
|
return []sdk.CheckState{*errSt}
|
|
}
|
|
var states []sdk.CheckState
|
|
|
|
// Per-endpoint TCP failures.
|
|
ldapsUp := ldapsReachable(data)
|
|
for _, ep := range data.Endpoints {
|
|
if ep.TCPConnected || ep.Error == "" {
|
|
continue
|
|
}
|
|
msg := "Cannot reach " + ep.Address + ": " + ep.Error + "."
|
|
if ep.Mode == ModePlain && ldapsUp {
|
|
states = append(states, infoState(
|
|
CodeTCPUnreachable, msg, ep.Address,
|
|
"LDAPS (636) is reachable, so modern clients are unaffected. Only relevant if legacy clients still need plain LDAP on 389.",
|
|
))
|
|
} else {
|
|
states = append(states, warnState(
|
|
CodeTCPUnreachable, msg, ep.Address,
|
|
"Verify firewall rules and that the LDAP server is listening on this address.",
|
|
))
|
|
}
|
|
}
|
|
|
|
// Aggregate: no endpoint reachable at all.
|
|
if len(data.Endpoints) > 0 {
|
|
allDown := true
|
|
for _, ep := range data.Endpoints {
|
|
if ep.TCPConnected {
|
|
allDown = false
|
|
break
|
|
}
|
|
}
|
|
if allDown {
|
|
states = append(states, critState(
|
|
CodeAllEndpointsDown,
|
|
"No LDAP endpoint is reachable.",
|
|
"",
|
|
"Check firewall rules and that the directory is listening on ports 389/636 (or the ports indicated by SRV).",
|
|
))
|
|
}
|
|
}
|
|
|
|
if len(states) == 0 {
|
|
return []sdk.CheckState{passState("ldap.endpoint_reachable.ok", "All discovered endpoints are reachable.")}
|
|
}
|
|
return states
|
|
}
|
|
|
|
// encryptedTransportRule: at least one endpoint is reachable AND encrypted.
|
|
type encryptedTransportRule struct{}
|
|
|
|
func (r *encryptedTransportRule) Name() string { return "ldap.has_encrypted_transport" }
|
|
func (r *encryptedTransportRule) Description() string {
|
|
return "Verifies that at least one reachable endpoint offers an encrypted channel (LDAPS or StartTLS)."
|
|
}
|
|
|
|
func (r *encryptedTransportRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
|
|
data, errSt := loadLDAPData(ctx, obs)
|
|
if errSt != nil {
|
|
return []sdk.CheckState{*errSt}
|
|
}
|
|
anyReachable := false
|
|
anyEncrypted := false
|
|
for _, ep := range data.Endpoints {
|
|
if ep.TCPConnected {
|
|
anyReachable = true
|
|
if ep.TLSEstablished {
|
|
anyEncrypted = true
|
|
}
|
|
}
|
|
}
|
|
if anyReachable && !anyEncrypted {
|
|
return []sdk.CheckState{critState(
|
|
CodeNoEncryptedEndpoint,
|
|
"None of the reachable endpoints established an encrypted channel (no StartTLS, no LDAPS).",
|
|
"",
|
|
"Enable either LDAPS (port 636) or StartTLS on 389 -- a bind DN should never travel in cleartext.",
|
|
)}
|
|
}
|
|
return []sdk.CheckState{passState("ldap.has_encrypted_transport.ok", "At least one endpoint offers encrypted transport.")}
|
|
}
|
|
|
|
// startTLSSupportedRule: StartTLS is advertised and succeeds on every
|
|
// reachable plain-LDAP endpoint.
|
|
type startTLSSupportedRule struct{}
|
|
|
|
func (r *startTLSSupportedRule) Name() string { return "ldap.starttls_supported" }
|
|
func (r *startTLSSupportedRule) Description() string {
|
|
return "Verifies that StartTLS is offered and succeeds on every reachable plain LDAP endpoint."
|
|
}
|
|
|
|
func (r *startTLSSupportedRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
|
|
data, errSt := loadLDAPData(ctx, obs)
|
|
if errSt != nil {
|
|
return []sdk.CheckState{*errSt}
|
|
}
|
|
var states []sdk.CheckState
|
|
for _, ep := range data.Endpoints {
|
|
if ep.Mode != ModePlain || !ep.TCPConnected {
|
|
continue
|
|
}
|
|
if !ep.StartTLSOffered {
|
|
states = append(states, critState(
|
|
CodeStartTLSMissing,
|
|
"StartTLS not advertised on "+ep.Address+".",
|
|
ep.Address,
|
|
"Configure the LDAP server to accept StartTLS (RFC 2830). On OpenLDAP, set olcTLSCertificateFile / olcTLSCertificateKeyFile and ensure the LDAP listener allows encrypted upgrades.",
|
|
))
|
|
} else if !ep.StartTLSUpgraded {
|
|
states = append(states, critState(
|
|
CodeStartTLSFailed,
|
|
"StartTLS handshake failed on "+ep.Address+": "+ep.Error+".",
|
|
ep.Address,
|
|
"Run the TLS checker on this endpoint for cert and cipher details.",
|
|
))
|
|
}
|
|
}
|
|
if len(states) == 0 {
|
|
return []sdk.CheckState{passState("ldap.starttls_supported.ok", "StartTLS works on every reachable plain LDAP endpoint.")}
|
|
}
|
|
return states
|
|
}
|
|
|
|
// ldapsHandshakeRule: the direct TLS handshake succeeds on every reachable
|
|
// LDAPS endpoint.
|
|
type ldapsHandshakeRule struct{}
|
|
|
|
func (r *ldapsHandshakeRule) Name() string { return "ldap.ldaps_handshake" }
|
|
func (r *ldapsHandshakeRule) Description() string {
|
|
return "Verifies that the direct TLS handshake succeeds on every LDAPS endpoint."
|
|
}
|
|
|
|
func (r *ldapsHandshakeRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
|
|
data, errSt := loadLDAPData(ctx, obs)
|
|
if errSt != nil {
|
|
return []sdk.CheckState{*errSt}
|
|
}
|
|
var states []sdk.CheckState
|
|
for _, ep := range data.Endpoints {
|
|
if ep.Mode == ModeLDAPS && ep.TCPConnected && !ep.TLSEstablished {
|
|
states = append(states, critState(
|
|
CodeLDAPSHandshakeFailed,
|
|
"LDAPS TLS handshake failed on "+ep.Address+": "+ep.Error+".",
|
|
ep.Address,
|
|
"Check that the LDAPS listener has a valid certificate/key pair and is speaking TLS directly (not LDAP+StartTLS).",
|
|
))
|
|
}
|
|
}
|
|
if len(states) == 0 {
|
|
return []sdk.CheckState{passState("ldap.ldaps_handshake.ok", "LDAPS handshake succeeds on every reachable LDAPS endpoint.")}
|
|
}
|
|
return states
|
|
}
|
|
|
|
// startTLSOnLDAPSRule: flags LDAPS endpoints that also advertise StartTLS.
|
|
type startTLSOnLDAPSRule struct{}
|
|
|
|
func (r *startTLSOnLDAPSRule) Name() string { return "ldap.starttls_on_ldaps" }
|
|
func (r *startTLSOnLDAPSRule) Description() string {
|
|
return "Flags servers that needlessly advertise StartTLS on the implicit-TLS LDAPS port."
|
|
}
|
|
|
|
func (r *startTLSOnLDAPSRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
|
|
data, errSt := loadLDAPData(ctx, obs)
|
|
if errSt != nil {
|
|
return []sdk.CheckState{*errSt}
|
|
}
|
|
var states []sdk.CheckState
|
|
for _, ep := range data.Endpoints {
|
|
if ep.Mode == ModeLDAPS && stringListContainsFold(ep.SupportedExtension, "1.3.6.1.4.1.1466.20037") {
|
|
states = append(states, infoState(
|
|
CodeStartTLSOnLDAPS,
|
|
"Server advertises StartTLS on the LDAPS port "+ep.Address+".",
|
|
ep.Address,
|
|
"This is not actively harmful, but most directories do not need StartTLS exposed on 636. Consider disabling it to reduce attack surface.",
|
|
))
|
|
}
|
|
}
|
|
if len(states) == 0 {
|
|
return []sdk.CheckState{passState("ldap.starttls_on_ldaps.ok", "LDAPS endpoints do not also advertise StartTLS.")}
|
|
}
|
|
return states
|
|
}
|
|
|
|
// ipv6ReachableRule: at least one endpoint reachable over IPv6.
|
|
type ipv6ReachableRule struct{}
|
|
|
|
func (r *ipv6ReachableRule) Name() string { return "ldap.ipv6_reachable" }
|
|
func (r *ipv6ReachableRule) Description() string {
|
|
return "Verifies at least one endpoint is reachable over IPv6."
|
|
}
|
|
|
|
func (r *ipv6ReachableRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
|
|
data, errSt := loadLDAPData(ctx, obs)
|
|
if errSt != nil {
|
|
return []sdk.CheckState{*errSt}
|
|
}
|
|
hasV4 := false
|
|
hasV6 := false
|
|
for _, ep := range data.Endpoints {
|
|
if !ep.TCPConnected {
|
|
continue
|
|
}
|
|
if ep.IsIPv6 {
|
|
hasV6 = true
|
|
} else {
|
|
hasV4 = true
|
|
}
|
|
}
|
|
if hasV4 && !hasV6 {
|
|
return []sdk.CheckState{infoState(
|
|
CodeNoIPv6,
|
|
"No IPv6 endpoint reachable.",
|
|
"",
|
|
"Publish AAAA records for the SRV targets.",
|
|
)}
|
|
}
|
|
return []sdk.CheckState{passState("ldap.ipv6_reachable.ok", "At least one endpoint is reachable over IPv6.")}
|
|
}
|