Initial commit
This commit is contained in:
commit
beca2fd7eb
21 changed files with 2698 additions and 0 deletions
279
checker/rules_transport.go
Normal file
279
checker/rules_transport.go
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
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.")}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue