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>
71 lines
2.4 KiB
Go
71 lines
2.4 KiB
Go
package validate
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"jarl/internal/model"
|
|
)
|
|
|
|
func TestDNSSEC(t *testing.T) {
|
|
b := newBuilder()
|
|
ok := b.primary("signed.example", nil)
|
|
ok.DNSSECPolicy = model.DNSSECDefault
|
|
ps := State(b.s, Options{})
|
|
if len(codes(ps, Error)) != 0 {
|
|
t.Fatalf("a signed zone: %v", ps)
|
|
}
|
|
|
|
sec := b.zone("copy.example", nil, model.ZoneSecondary)
|
|
b.opt(model.ScopeZone, sec.ID, "primaries", "{ 192.0.2.1; }")
|
|
sec.DNSSECPolicy = model.DNSSECDefault
|
|
unknown := b.primary("custom.example", nil)
|
|
unknown.DNSSECPolicy = "fast"
|
|
long := b.primary("long.example", nil)
|
|
long.DNSSECPolicy = model.DNSSECInsecure
|
|
r := b.rec(long, "www", "A", "192.0.2.80")
|
|
r.TTL = model.Ptr(uint32(172800))
|
|
b.opt(model.ScopeZone, ok.ID, "inline-signing", "no")
|
|
ps = State(b.s, Options{})
|
|
for _, c := range []string{"dnssec_zone_type", "dnssec_policy_unknown", "dnssec_ttl_too_long", "dnssec_inline_signing"} {
|
|
if !hasCode(ps, c) {
|
|
t.Errorf("missing %s in %v", c, ps)
|
|
}
|
|
}
|
|
|
|
// A custom policy: its max-zone-ttl applies; an empty keys list would
|
|
// crash named.
|
|
b2 := newBuilder()
|
|
z := b2.primary("custom.example", nil)
|
|
z.DNSSECPolicy = "fast"
|
|
z.DefaultTTL = 600
|
|
b2.opt(model.ScopeTopLevel, 0, "dnssec-policy", `fast { keys { csk lifetime unlimited algorithm 13; }; max-zone-ttl PT5M; }`)
|
|
b2.opt(model.ScopeTopLevel, 0, "dnssec-policy", `"off" { keys { }; }`)
|
|
ps = State(b2.s, Options{})
|
|
if !hasCode(ps, "dnssec_ttl_too_long") || !hasCode(ps, "dnssec_policy_no_keys") || hasCode(ps, "dnssec_policy_unknown") {
|
|
t.Errorf("custom policy: %v", ps)
|
|
}
|
|
|
|
// dnssec-policy is set per zone in Jarl, not as an option.
|
|
b3 := newBuilder()
|
|
z = b3.primary("example.com", nil)
|
|
b3.opt(model.ScopeZone, z.ID, "dnssec-policy", "default")
|
|
b3.opt(model.ScopeOptions, 0, "dnssec-policy", "default")
|
|
b3.opt(model.ScopeZone, z.ID, "key-directory", `"/tmp"`)
|
|
if n := len(codes(State(b3.s, Options{}), Error)); n != 3 {
|
|
t.Errorf("managed options: %v", State(b3.s, Options{}))
|
|
}
|
|
}
|
|
|
|
func TestParseDuration(t *testing.T) {
|
|
for in, want := range map[string]uint32{"86400": 86400, "P1D": 86400, "PT5M": 300, "P1W": 604800, "1d": 86400, "12h30m": 45000, "pt1h": 3600} {
|
|
if got, ok := parseDuration(in); !ok || got != want {
|
|
t.Errorf("parseDuration(%q) = %d %v, want %d", in, got, ok, want)
|
|
}
|
|
}
|
|
for _, bad := range []string{"", "P", "unlimited", "1x"} {
|
|
if _, ok := parseDuration(bad); ok {
|
|
t.Errorf("parseDuration(%q) accepted", bad)
|
|
}
|
|
}
|
|
}
|