Initial commit
This commit is contained in:
commit
40a4cf285e
18 changed files with 1933 additions and 0 deletions
224
checker/rule.go
Normal file
224
checker/rule.go
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
package checker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
// Rule returns the kerberos_health rule.
|
||||
func Rule() sdk.CheckRule {
|
||||
return &kerberosRule{}
|
||||
}
|
||||
|
||||
type kerberosRule struct{}
|
||||
|
||||
func (r *kerberosRule) Name() string {
|
||||
return "kerberos_health"
|
||||
}
|
||||
|
||||
func (r *kerberosRule) Description() string {
|
||||
return "Checks whether a Kerberos realm answers correctly, advertises strong crypto, and exposes no obvious misconfiguration."
|
||||
}
|
||||
|
||||
func (r *kerberosRule) ValidateOptions(opts sdk.CheckerOptions) error { return nil }
|
||||
|
||||
func (r *kerberosRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
|
||||
var data KerberosData
|
||||
if err := obs.Get(ctx, ObservationKeyKerberos, &data); err != nil {
|
||||
return []sdk.CheckState{{
|
||||
Status: sdk.StatusError,
|
||||
Message: fmt.Sprintf("Failed to get Kerberos data: %v", err),
|
||||
Code: "kerberos_error",
|
||||
}}
|
||||
}
|
||||
|
||||
maxSkew := time.Duration(optFloat(opts, "maxClockSkew", 300)) * time.Second
|
||||
requireStrong := optBool(opts, "requireStrongEnctypes", true)
|
||||
|
||||
// Presence of at least one _kerberos._tcp or ._udp record is mandatory.
|
||||
hasKDCSRV := false
|
||||
for _, b := range data.SRV {
|
||||
if strings.HasPrefix(b.Prefix, "_kerberos.") && len(b.Records) > 0 {
|
||||
hasKDCSRV = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasKDCSRV {
|
||||
return []sdk.CheckState{{
|
||||
Status: sdk.StatusCrit,
|
||||
Subject: data.Realm,
|
||||
Message: fmt.Sprintf("No _kerberos SRV records found for %s", data.Realm),
|
||||
Code: "kerberos_no_srv",
|
||||
}}
|
||||
}
|
||||
|
||||
// KDC reachability: need at least one successful probe among KDC roles.
|
||||
reachable := 0
|
||||
kdcProbes := 0
|
||||
kadminDown, kpasswdDown := false, false
|
||||
for _, p := range data.Probes {
|
||||
switch p.Role {
|
||||
case "kdc":
|
||||
kdcProbes++
|
||||
if p.OK {
|
||||
reachable++
|
||||
}
|
||||
case "kadmin":
|
||||
if !p.OK {
|
||||
kadminDown = true
|
||||
}
|
||||
case "kpasswd":
|
||||
if !p.OK {
|
||||
kpasswdDown = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if reachable == 0 {
|
||||
return []sdk.CheckState{{
|
||||
Status: sdk.StatusCrit,
|
||||
Subject: data.Realm,
|
||||
Message: "No KDC is reachable on TCP 88 or UDP 88",
|
||||
Code: "kerberos_kdc_unreachable",
|
||||
}}
|
||||
}
|
||||
|
||||
// AS-REQ result.
|
||||
if data.AS.Attempted {
|
||||
if data.AS.Error != "" && data.AS.ErrorCode == 0 {
|
||||
return []sdk.CheckState{{
|
||||
Status: sdk.StatusCrit,
|
||||
Subject: data.Realm,
|
||||
Message: "AS-REQ probe failed: " + data.AS.Error,
|
||||
Code: "kerberos_error",
|
||||
}}
|
||||
}
|
||||
if data.AS.ServerRealm != "" && !strings.EqualFold(data.AS.ServerRealm, data.Realm) {
|
||||
return []sdk.CheckState{{
|
||||
Status: sdk.StatusCrit,
|
||||
Subject: data.Realm,
|
||||
Message: fmt.Sprintf("KDC replied for realm %q, expected %q", data.AS.ServerRealm, data.Realm),
|
||||
Code: "kerberos_wrong_realm",
|
||||
}}
|
||||
}
|
||||
if abs(data.AS.ClockSkew) > maxSkew {
|
||||
return []sdk.CheckState{{
|
||||
Status: sdk.StatusCrit,
|
||||
Subject: data.Realm,
|
||||
Message: fmt.Sprintf("Clock skew with KDC is %s (max %s)", round(data.AS.ClockSkew), maxSkew),
|
||||
Code: "kerberos_clock_skew",
|
||||
Meta: map[string]any{
|
||||
"skew_ns": data.AS.ClockSkew.Nanoseconds(),
|
||||
},
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
// Crypto posture.
|
||||
hasStrong := false
|
||||
for _, e := range data.Enctypes {
|
||||
if !e.Weak {
|
||||
hasStrong = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if requireStrong && len(data.Enctypes) > 0 && !hasStrong {
|
||||
return []sdk.CheckState{{
|
||||
Status: sdk.StatusCrit,
|
||||
Subject: data.Realm,
|
||||
Message: "KDC only advertises weak enctypes (DES/RC4)",
|
||||
Code: "kerberos_weak_crypto",
|
||||
}}
|
||||
}
|
||||
|
||||
// Auth probe (if any).
|
||||
if data.Auth != nil && data.Auth.Attempted {
|
||||
if !data.Auth.TGTAcquired {
|
||||
return []sdk.CheckState{{
|
||||
Status: sdk.StatusCrit,
|
||||
Subject: data.Realm,
|
||||
Message: "Authenticated probe: TGT acquisition failed",
|
||||
Code: "kerberos_auth_fail",
|
||||
}}
|
||||
}
|
||||
if data.Auth.TargetService != "" && !data.Auth.TGSAcquired {
|
||||
return []sdk.CheckState{{
|
||||
Status: sdk.StatusWarn,
|
||||
Subject: data.Realm,
|
||||
Message: fmt.Sprintf("TGT OK but TGS-REQ for %s failed", data.Auth.TargetService),
|
||||
Code: "kerberos_tgs_fail",
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
// Warnings: partial reachability, no UDP, mixed crypto, no preauth.
|
||||
var warnings []string
|
||||
if reachable < kdcProbes {
|
||||
warnings = append(warnings, fmt.Sprintf("%d/%d KDC endpoints unreachable", kdcProbes-reachable, kdcProbes))
|
||||
}
|
||||
if len(data.WeakEnctypes) > 0 && hasStrong {
|
||||
warnings = append(warnings, "KDC also advertises weak enctypes alongside strong ones")
|
||||
}
|
||||
if data.AS.Attempted && data.AS.PrincipalFound && !data.AS.PreauthReq {
|
||||
warnings = append(warnings, "AS-REP returned without preauth (AS-REP roasting exposure)")
|
||||
}
|
||||
if kadminDown {
|
||||
warnings = append(warnings, "kadmin server unreachable")
|
||||
}
|
||||
if kpasswdDown {
|
||||
warnings = append(warnings, "kpasswd unreachable")
|
||||
}
|
||||
|
||||
if len(warnings) > 0 {
|
||||
return []sdk.CheckState{{
|
||||
Status: sdk.StatusWarn,
|
||||
Subject: data.Realm,
|
||||
Message: fmt.Sprintf("Realm %s reachable: %s", data.Realm, strings.Join(warnings, "; ")),
|
||||
Code: "kerberos_warn",
|
||||
Meta: map[string]any{
|
||||
"reachable_kdcs": reachable,
|
||||
"warnings": warnings,
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
return []sdk.CheckState{{
|
||||
Status: sdk.StatusOK,
|
||||
Subject: data.Realm,
|
||||
Message: fmt.Sprintf("Realm %s healthy (%d KDC reachable, strong crypto)", data.Realm, reachable),
|
||||
Code: "kerberos_ok",
|
||||
Meta: map[string]any{
|
||||
"realm": data.Realm,
|
||||
"reachable_kdcs": reachable,
|
||||
"clock_skew_ns": data.AS.ClockSkew.Nanoseconds(),
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
func abs(d time.Duration) time.Duration {
|
||||
if d < 0 {
|
||||
return -d
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func round(d time.Duration) time.Duration {
|
||||
return d.Round(time.Millisecond)
|
||||
}
|
||||
|
||||
func optBool(opts sdk.CheckerOptions, key string, def bool) bool {
|
||||
v, ok := opts[key]
|
||||
if !ok {
|
||||
return def
|
||||
}
|
||||
switch x := v.(type) {
|
||||
case bool:
|
||||
return x
|
||||
case string:
|
||||
return x == "true" || x == "1"
|
||||
}
|
||||
return def
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue