checker-dnssec/checker/rules_signatures.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))
}