162 lines
5.9 KiB
Go
162 lines
5.9 KiB
Go
package checker
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
sdk "git.happydns.org/checker-sdk-go/checker"
|
|
)
|
|
|
|
// rrsigPresentDNSKEYRule catches the most opaque DNSSEC failure: an answer
|
|
// with DNSKEYs but no covering RRSIG, which makes the zone unverifiable.
|
|
type rrsigPresentDNSKEYRule struct{}
|
|
|
|
func (rrsigPresentDNSKEYRule) Name() string { return "dnssec_rrsig_present_dnskey" }
|
|
func (rrsigPresentDNSKEYRule) Description() string {
|
|
return "Ensures the DNSKEY RRset is signed."
|
|
}
|
|
|
|
func (rrsigPresentDNSKEYRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
|
|
data, errState := loadDNSSEC(ctx, obs)
|
|
if errState != nil {
|
|
return errState
|
|
}
|
|
if !hasAnyDNSKEY(data) {
|
|
return skipped("zone not signed")
|
|
}
|
|
for _, name := range sortedServers(data) {
|
|
v := data.Servers[name]
|
|
if len(v.DNSKEYs) > 0 && len(v.DNSKEYRRSIGs) == 0 {
|
|
return []sdk.CheckState{withMeta(sdk.CheckState{
|
|
Status: sdk.StatusCrit,
|
|
Subject: name,
|
|
Message: fmt.Sprintf("server %s returned DNSKEYs but no covering RRSIG; validators will SERVFAIL", name),
|
|
}, "Re-sign the zone and check the signer's KSK access; an expired or revoked KSK silently produces this state.",
|
|
"dnssec.dnskey_unsigned")}
|
|
}
|
|
}
|
|
return okState(data.Domain, "DNSKEY RRset is signed on every server")
|
|
}
|
|
|
|
type rrsigPresentSOARule struct{}
|
|
|
|
func (rrsigPresentSOARule) Name() string { return "dnssec_rrsig_present_soa" }
|
|
func (rrsigPresentSOARule) Description() string {
|
|
return "Ensures the SOA RRset is signed."
|
|
}
|
|
|
|
func (rrsigPresentSOARule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
|
|
data, errState := loadDNSSEC(ctx, obs)
|
|
if errState != nil {
|
|
return errState
|
|
}
|
|
if !hasAnyDNSKEY(data) {
|
|
return skipped("zone not signed")
|
|
}
|
|
for _, name := range sortedServers(data) {
|
|
v := data.Servers[name]
|
|
if v.SOA != nil && len(v.SOARRSIGs) == 0 {
|
|
return []sdk.CheckState{withMeta(sdk.CheckState{
|
|
Status: sdk.StatusCrit,
|
|
Subject: name,
|
|
Message: fmt.Sprintf("server %s returned a SOA but no covering RRSIG", name),
|
|
}, "Re-sign the zone; an unsigned SOA in a signed zone breaks every NXDOMAIN proof.",
|
|
"dnssec.soa_unsigned")}
|
|
}
|
|
}
|
|
return okState(data.Domain, "SOA RRset is signed on every server")
|
|
}
|
|
|
|
type rrsigValidityWindowRule struct{}
|
|
|
|
func (rrsigValidityWindowRule) Name() string { return "dnssec_rrsig_validity_window" }
|
|
func (rrsigValidityWindowRule) Description() string {
|
|
return "Verifies that every observed RRSIG is currently within [Inception, Expiration]."
|
|
}
|
|
|
|
func (rrsigValidityWindowRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, _ sdk.CheckerOptions) []sdk.CheckState {
|
|
data, errState := loadDNSSEC(ctx, obs)
|
|
if errState != nil {
|
|
return errState
|
|
}
|
|
now := uint32(time.Now().UTC().Unix())
|
|
for _, name := range sortedServers(data) {
|
|
v := data.Servers[name]
|
|
for _, sig := range v.AllRRSIGs() {
|
|
// Inception/Expiration are unsigned 32-bit serial-arithmetic
|
|
// timestamps. A naive < / > would mishandle the year-2106 wrap;
|
|
// we use signed-difference comparison instead.
|
|
if int32(now-sig.Inception) < 0 {
|
|
return []sdk.CheckState{withMeta(sdk.CheckState{
|
|
Status: sdk.StatusCrit,
|
|
Subject: name,
|
|
Message: fmt.Sprintf("RRSIG (KeyTag %d, type %d) on %s has not yet entered its validity window", sig.KeyTag, sig.TypeCovered, name),
|
|
}, "Check the signer's clock; future-dated inceptions usually mean a misconfigured NTP.",
|
|
"dnssec.rrsig_outside_window")}
|
|
}
|
|
if int32(sig.Expiration-now) < 0 {
|
|
return []sdk.CheckState{withMeta(sdk.CheckState{
|
|
Status: sdk.StatusCrit,
|
|
Subject: name,
|
|
Message: fmt.Sprintf("RRSIG (KeyTag %d, type %d) on %s is already expired", sig.KeyTag, sig.TypeCovered, name),
|
|
}, "Re-sign the zone immediately and check the signing cron; this is the most common cause of sudden DNSSEC outages.",
|
|
"dnssec.rrsig_outside_window")}
|
|
}
|
|
}
|
|
}
|
|
return okState(data.Domain, "all RRSIGs are within their validity window")
|
|
}
|
|
|
|
type rrsigFreshnessRule struct{}
|
|
|
|
func (rrsigFreshnessRule) Name() string { return "dnssec_rrsig_freshness" }
|
|
func (rrsigFreshnessRule) Description() string {
|
|
return "Warns when RRSIGs are close to expiring; preemptive alert for stuck signers."
|
|
}
|
|
|
|
func (rrsigFreshnessRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
|
|
data, errState := loadDNSSEC(ctx, obs)
|
|
if errState != nil {
|
|
return errState
|
|
}
|
|
warnDays := optionUint(opts, "signatureFreshness", defaultSignatureFreshnessDays)
|
|
critDays := optionUint(opts, "signatureFreshnessCrit", defaultSignatureFreshnessCrit)
|
|
|
|
now := time.Now().UTC().Unix()
|
|
var minRemaining int64 = 1 << 30
|
|
var minSubject string
|
|
var minSig RRSIGObservation
|
|
found := false
|
|
for _, name := range sortedServers(data) {
|
|
v := data.Servers[name]
|
|
for _, sig := range v.AllRRSIGs() {
|
|
diff := int64(int32(sig.Expiration - uint32(now)))
|
|
if !found || diff < minRemaining {
|
|
minRemaining = diff
|
|
minSubject = name
|
|
minSig = sig
|
|
found = true
|
|
}
|
|
}
|
|
}
|
|
if !found {
|
|
return skipped("no RRSIG observed")
|
|
}
|
|
days := minRemaining / 86400
|
|
switch {
|
|
case days < int64(critDays):
|
|
return []sdk.CheckState{withMeta(sdk.CheckState{
|
|
Status: sdk.StatusCrit,
|
|
Subject: minSubject,
|
|
Message: fmt.Sprintf("RRSIG on %s (KeyTag %d) expires in %d hours", minSubject, minSig.KeyTag, minRemaining/3600),
|
|
}, "Check the signing cron: this is hours away from causing a SERVFAIL outage.", "dnssec.rrsig_close_to_expiry")}
|
|
case days < int64(warnDays):
|
|
return []sdk.CheckState{withMeta(sdk.CheckState{
|
|
Status: sdk.StatusWarn,
|
|
Subject: minSubject,
|
|
Message: fmt.Sprintf("nearest RRSIG (KeyTag %d on %s) expires in %d days", minSig.KeyTag, minSubject, days),
|
|
}, "Verify the signer's resigning interval; less than a week of headroom leaves no margin for a stuck cron.", "dnssec.rrsig_close_to_expiry")}
|
|
}
|
|
return okState(data.Domain, fmt.Sprintf("nearest RRSIG expires in %d days", days))
|
|
}
|