Add DNSSEC signing, key backups and Porkbun support
Zones get a DNSSEC setting: named signs them with BIND's default (or a custom) dnssec-policy and inline signing, with keys in a per-view key directory. Jarl shows the keys, the DS record and when it is safe to publish it, checks the parent's DS records (check-ds, which also tells named), and refuses steps that would break a signed domain: switching straight to unsigned, finishing a turn-off too early, deleting or moving a signed zone, TTLs above max-zone-ttl. Turning off goes through the insecure policy; wipe starts over once the parent has no DS. Backups store the DNSSEC keys and restore puts them back for named; the backup unit gets CAP_DAC_READ_SEARCH to read them. Jarl raises a zone's serial above named's whenever it writes an older one (after a restore). The serials check compares the signed serial. With a Porkbun API key in jarl.yaml, the registrar's name servers, glue records and DS records can be shown and changed, each change checked first (dry run, force to override). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
293e6ddd33
commit
5630e7a848
72 files changed
+5760
-153
No files matched your search
@@ -10,6 +10,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"jarl/internal/dnssec"
|
||||
)
|
||||
|
||||
// fake is a DNS server on one loopback address that answers with fn.
|
||||
@@ -187,7 +189,7 @@ func TestDelegationMismatch(t *testing.T) {
|
||||
ref := uint32(7)
|
||||
rep := checker(port, res).Delegation(context.Background(), "adbraun.lol.",
|
||||
[]string{"ns1.adbraun.lol.", "ns2.adbraun.lol."},
|
||||
map[string][]string{"ns1.adbraun.lol.": {"127.0.0.99"}, "ns2.adbraun.lol.": {"127.0.0.14"}}, &ref)
|
||||
map[string][]string{"ns1.adbraun.lol.": {"127.0.0.99"}, "ns2.adbraun.lol.": {"127.0.0.14"}}, &ref, nil)
|
||||
if rep.Parent != "lol." || !rep.Delegated {
|
||||
t.Fatalf("report: %+v", rep)
|
||||
}
|
||||
@@ -218,7 +220,7 @@ func TestDelegationGood(t *testing.T) {
|
||||
return m
|
||||
})
|
||||
ref := uint32(7)
|
||||
rep := checker(port, res).Delegation(context.Background(), "adbraun.lol.", []string{"ns1.adbraun.lol."}, map[string][]string{"ns1.adbraun.lol.": {"127.0.0.12"}}, &ref)
|
||||
rep := checker(port, res).Delegation(context.Background(), "adbraun.lol.", []string{"ns1.adbraun.lol."}, map[string][]string{"ns1.adbraun.lol.": {"127.0.0.12"}}, &ref, nil)
|
||||
l := levels(rep)
|
||||
if len(l["error"])+len(l["warning"])+len(l["info"]) != 0 || len(l["ok"]) != 2 {
|
||||
t.Errorf("findings: %v", l)
|
||||
@@ -234,7 +236,7 @@ func TestNotDelegated(t *testing.T) {
|
||||
m.Ns = []dns.RR{rr("lol. 300 IN SOA ns.nic.lol. hostmaster.lol. 1 3600 900 1209600 300")}
|
||||
return m
|
||||
})
|
||||
rep := checker(port, res).Delegation(context.Background(), "adbraun.lol.", []string{"ns1.adbraun.lol."}, nil, nil)
|
||||
rep := checker(port, res).Delegation(context.Background(), "adbraun.lol.", []string{"ns1.adbraun.lol."}, nil, nil, nil)
|
||||
l := levels(rep)
|
||||
if rep.Delegated || len(l["info"]) != 1 || len(l["warning"])+len(l["error"]) != 0 {
|
||||
t.Errorf("findings: %v", l)
|
||||
@@ -243,3 +245,65 @@ func TestNotDelegated(t *testing.T) {
|
||||
t.Errorf("note: %s", l["info"][0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelegationDS(t *testing.T) {
|
||||
const dsData = "22642 13 2 0B7A2F5E1C8E2F6A0B7A2F5E1C8E2F6A0B7A2F5E1C8E2F6A0B7A2F5E1C8E2F6A"
|
||||
parent := func(withDS bool) fake {
|
||||
return func(q dns.Question) *dns.Msg {
|
||||
m := new(dns.Msg)
|
||||
switch {
|
||||
case q.Qtype == dns.TypeNS && q.Name == "adbraun.lol.":
|
||||
m.Ns = []dns.RR{rr("adbraun.lol. 3600 IN NS ns1.adbraun.lol.")}
|
||||
m.Extra = []dns.RR{rr("ns1.adbraun.lol. 3600 IN A 127.0.0.12")}
|
||||
case q.Qtype == dns.TypeDS && q.Name == "adbraun.lol.":
|
||||
m.Authoritative = true
|
||||
if withDS {
|
||||
m.Answer = []dns.RR{rr("adbraun.lol. 3600 IN DS " + dsData)}
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
}
|
||||
ours := dnssec.DS{KeyTag: 22642, Algorithm: 13, DigestType: 2, Digest: strings.Fields(dsData)[3]}
|
||||
other := ours
|
||||
other.KeyTag, other.Digest = 1111, strings.Repeat("AB", 32)
|
||||
ref := uint32(7)
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
withDS bool
|
||||
sig Signing
|
||||
level string
|
||||
want string
|
||||
}{
|
||||
{"secure", true, Signing{Policy: "default", DS: []dnssec.DS{ours}}, "ok", "matches the zone's key 22642"},
|
||||
{"wrong key", true, Signing{Policy: "default", DS: []dnssec.DS{other}}, "error", "matches none of the zone's keys"},
|
||||
{"unsigned", true, Signing{}, "error", "but the zone isn't signed"},
|
||||
{"turning off", true, Signing{Policy: "insecure", DS: []dnssec.DS{ours}}, "warning", "remove it at your registrar"},
|
||||
{"no DS yet", false, Signing{Policy: "default", DS: []dnssec.DS{ours}}, "info", "the parent has no DS record for it"},
|
||||
{"DS gone", false, Signing{Policy: "insecure"}, "ok", "turning DNSSEC off can go ahead"},
|
||||
} {
|
||||
port, res := internet(t, parent(c.withDS))
|
||||
sig := c.sig
|
||||
rep := checker(port, res).Delegation(context.Background(), "adbraun.lol.", []string{"ns1.adbraun.lol."}, map[string][]string{"ns1.adbraun.lol.": {"127.0.0.12"}}, &ref, &sig)
|
||||
found := false
|
||||
for _, f := range rep.Findings {
|
||||
if f.Level == c.level && strings.Contains(f.Message, c.want) {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("%s: no %s finding %q in %+v", c.name, c.level, c.want, rep.Findings)
|
||||
}
|
||||
if c.withDS && (len(rep.ParentDS) != 1 || rep.ParentDS[0].KeyTag != 22642) {
|
||||
t.Errorf("%s: parent DS %+v", c.name, rep.ParentDS)
|
||||
}
|
||||
}
|
||||
// Unsigned and no DS: nothing about DNSSEC.
|
||||
port, res := internet(t, parent(false))
|
||||
rep := checker(port, res).Delegation(context.Background(), "adbraun.lol.", []string{"ns1.adbraun.lol."}, map[string][]string{"ns1.adbraun.lol.": {"127.0.0.12"}}, &ref, &Signing{})
|
||||
for _, f := range rep.Findings {
|
||||
if strings.Contains(f.Message, "DS") {
|
||||
t.Errorf("unsigned zone: %+v", f)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user