// 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 }