214 lines
6.8 KiB
Go
214 lines
6.8 KiB
Go
// SPDX-License-Identifier: MIT
|
|
|
|
package checker
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
|
|
sdk "git.happydns.org/checker-sdk-go/checker"
|
|
)
|
|
|
|
// commonFailuresRule uses substring matching rather than exact codes because
|
|
// DNSViz wording shifts between versions.
|
|
type commonFailuresRule struct{}
|
|
|
|
func (r *commonFailuresRule) Name() string { return "dnsviz_common_failures" }
|
|
func (r *commonFailuresRule) Description() string {
|
|
return "Highlights well-known DNSSEC failure scenarios (broken chain, expired signatures, missing/extra DS, algorithm mismatch, …) with remediation hints."
|
|
}
|
|
|
|
// CommonFailure is exported so the report layer can read Title/Hint from CheckState.Meta.
|
|
type CommonFailure struct {
|
|
ID string // Stable code emitted in CheckState.Code.
|
|
Title string // Short headline for the report block.
|
|
Hint string // What the user should typically do.
|
|
Patterns []string // Substrings (lowercased) matched against the finding's code+description.
|
|
Severity sdk.Status
|
|
}
|
|
|
|
// commonFailures is the curated catalog. Order matters: the first matching
|
|
// entry wins, so put more specific scenarios above the generic ones.
|
|
var commonFailures = []CommonFailure{
|
|
{
|
|
ID: "dnssec_chain_broken_no_ds",
|
|
Title: "Parent has no DS record for this zone",
|
|
Hint: "Publish the DS record(s) generated from your KSK at the registrar/parent. Without DS, validators see the zone as INSECURE even when DNSKEYs are present.",
|
|
Patterns: []string{
|
|
"no ds records",
|
|
"missing ds",
|
|
"no ds record was found",
|
|
"ds_records_missing",
|
|
},
|
|
Severity: sdk.StatusCrit,
|
|
},
|
|
{
|
|
ID: "dnssec_ds_digest_mismatch",
|
|
Title: "DS at parent does not match any DNSKEY at the child",
|
|
Hint: "The DS digest at the parent does not match any DNSKEY served by the child. Re-export the DS record from your current KSK and update it at the registrar; remove stale DS entries.",
|
|
Patterns: []string{
|
|
"ds does not match",
|
|
"no matching dnskey",
|
|
"ds_does_not_match",
|
|
"no dnskey matching",
|
|
},
|
|
Severity: sdk.StatusCrit,
|
|
},
|
|
{
|
|
ID: "dnssec_rrsig_expired",
|
|
Title: "RRSIG signature has expired",
|
|
Hint: "At least one RRSIG is past its expiration. Resign the zone (most signers do this automatically; investigate why the cron/automation didn't run).",
|
|
Patterns: []string{
|
|
"signature has expired",
|
|
"rrsig_expired",
|
|
"signature_expired",
|
|
"signature expired",
|
|
"rrsig expired",
|
|
},
|
|
Severity: sdk.StatusCrit,
|
|
},
|
|
{
|
|
ID: "dnssec_rrsig_not_yet_valid",
|
|
Title: "RRSIG signature is not yet valid",
|
|
Hint: "An RRSIG inception time is in the future. Check that the signing host's clock is synchronized (NTP) and that the signer didn't generate signatures with a future inception.",
|
|
Patterns: []string{
|
|
"signature is not yet valid",
|
|
"signature_not_yet_valid",
|
|
"inception in the future",
|
|
},
|
|
Severity: sdk.StatusCrit,
|
|
},
|
|
{
|
|
ID: "dnssec_signature_invalid",
|
|
Title: "Cryptographic signature is invalid",
|
|
Hint: "A validator could not verify the signature with the published DNSKEY. The zone may have been resigned with a key that was not published, or the served DNSKEY set is inconsistent across servers.",
|
|
Patterns: []string{
|
|
"signature_invalid",
|
|
"signature is invalid",
|
|
"bad signature",
|
|
},
|
|
Severity: sdk.StatusCrit,
|
|
},
|
|
{
|
|
ID: "dnssec_algorithm_mismatch",
|
|
Title: "Algorithm declared in DS not present in DNSKEY (or vice versa)",
|
|
Hint: "RFC 4035 §2.2 requires that for every algorithm a DS uses, the child must publish at least one DNSKEY with the same algorithm. Either add the missing DNSKEY/DS or retire the orphan.",
|
|
Patterns: []string{
|
|
"algorithm_missing",
|
|
"algorithm not signed",
|
|
"missing rrsig for algorithm",
|
|
"algorithm mismatch",
|
|
},
|
|
Severity: sdk.StatusCrit,
|
|
},
|
|
{
|
|
ID: "dnssec_deprecated_algorithm",
|
|
Title: "Deprecated DNSSEC algorithm in use",
|
|
Hint: "Algorithms 5 (RSASHA1) and 7 (RSASHA1-NSEC3) are deprecated. Roll the KSK/ZSK to algorithm 13 (ECDSAP256SHA256) or 8 (RSASHA256) and update the DS at the parent.",
|
|
Patterns: []string{
|
|
"deprecated algorithm",
|
|
"algorithm_deprecated",
|
|
"weak algorithm",
|
|
"rsasha1",
|
|
"rsa/sha-1",
|
|
"rsa-sha1",
|
|
"algorithm 5 ",
|
|
"algorithm 7 ",
|
|
},
|
|
Severity: sdk.StatusWarn,
|
|
},
|
|
{
|
|
ID: "dnssec_no_dnskey",
|
|
Title: "No DNSKEY served at the apex",
|
|
Hint: "The zone declares a DS at the parent but serves no DNSKEY at the apex. Validators see this as BOGUS. Republish the DNSKEY set or remove the DS at the parent.",
|
|
Patterns: []string{
|
|
"no dnskey",
|
|
"dnskey_missing",
|
|
},
|
|
Severity: sdk.StatusCrit,
|
|
},
|
|
{
|
|
ID: "dnssec_servfail",
|
|
Title: "An authoritative server returned SERVFAIL on DNSSEC queries",
|
|
Hint: "At least one server on the path returned SERVFAIL. Often caused by a server that doesn't have the keys it should sign with, or by EDNS/UDP fragmentation. Verify the server can answer DNSKEY/RRSIG over both UDP and TCP.",
|
|
Patterns: []string{
|
|
"servfail",
|
|
"server failure",
|
|
},
|
|
Severity: sdk.StatusCrit,
|
|
},
|
|
{
|
|
ID: "dnssec_inconsistent_responses",
|
|
Title: "Authoritative servers disagree",
|
|
Hint: "Different authoritative servers serve different DNSKEY/RRSIG/NSEC contents. Confirm that the secondary servers have completed AXFR/IXFR and are serving the same zone version.",
|
|
Patterns: []string{
|
|
"inconsistent",
|
|
"disagree",
|
|
},
|
|
Severity: sdk.StatusWarn,
|
|
},
|
|
}
|
|
|
|
func (r *commonFailuresRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) []sdk.CheckState {
|
|
data, errState := loadData(ctx, obs, "dnsviz_common_failures")
|
|
if errState != nil {
|
|
return errState
|
|
}
|
|
var out []sdk.CheckState
|
|
matched := map[string]struct{}{}
|
|
matchFinding := func(name string, f Finding) {
|
|
haystack := strings.ToLower(f.Code + " " + f.Description)
|
|
for _, c := range commonFailures {
|
|
if !matchesAny(haystack, c.Patterns) {
|
|
continue
|
|
}
|
|
key := name + "|" + c.ID
|
|
if _, seen := matched[key]; seen {
|
|
return
|
|
}
|
|
matched[key] = struct{}{}
|
|
out = append(out, sdk.CheckState{
|
|
Status: c.Severity,
|
|
Code: c.ID,
|
|
Subject: name,
|
|
Message: c.Title + ": " + c.Hint,
|
|
Meta: map[string]any{
|
|
"title": c.Title,
|
|
"hint": c.Hint,
|
|
"original_code": f.Code,
|
|
"original_description": f.Description,
|
|
},
|
|
})
|
|
return
|
|
}
|
|
}
|
|
for _, name := range orderedZones(data) {
|
|
z := data.Zones[name]
|
|
for _, f := range z.Errors {
|
|
matchFinding(name, f)
|
|
}
|
|
for _, f := range z.Warnings {
|
|
matchFinding(name, f)
|
|
}
|
|
}
|
|
if len(out) == 0 {
|
|
return []sdk.CheckState{{
|
|
Status: sdk.StatusOK,
|
|
Code: "dnsviz_common_failures",
|
|
Message: "No well-known DNSSEC failure scenario detected by the heuristics.",
|
|
}}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func matchesAny(haystack string, needles []string) bool {
|
|
for _, n := range needles {
|
|
if n == "" {
|
|
continue
|
|
}
|
|
if strings.Contains(haystack, n) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|