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>
171 lines
6.8 KiB
Go
171 lines
6.8 KiB
Go
package dnssec
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Phases of a zone, from unsigned to signed and back. Turning signing on:
|
|
// off -> starting -> signing -> ready (add the DS at the parent) ->
|
|
// ds_pending -> secure. Turning it off: secure -> removing_ds (remove the
|
|
// DS at the parent) -> ds_expiring -> removing_keys -> unsigned -> off.
|
|
const (
|
|
PhaseOff = "off" // not signed, no keys
|
|
PhaseLeftover = "leftover_keys" // not signed, but old keys are still in the key directory
|
|
PhaseStarting = "starting" // signing is on in Jarl; named hasn't made keys yet
|
|
PhaseSigning = "signing" // keys made, zone signed; not yet safe to add the DS
|
|
PhaseReady = "ready" // safe to add the DS at the parent
|
|
PhaseDSPending = "ds_pending" // the DS is at the parent; named waits for its TTL
|
|
PhaseSecure = "secure" // signed, and the parent's DS is in place
|
|
PhaseRemovingDS = "removing_ds" // turning off: the DS must go from the parent first
|
|
PhaseDSExpiring = "ds_expiring" // the DS is gone; named waits until caches forget it
|
|
PhaseRemovingKeys = "removing_keys" // named removes the keys and signatures
|
|
PhaseUnsigned = "unsigned" // turning off has finished; switch DNSSEC off
|
|
)
|
|
|
|
// Phase says where a zone is and what comes next.
|
|
type Phase struct {
|
|
Code string `json:"code"`
|
|
Summary string `json:"summary"`
|
|
Next string `json:"next,omitempty"`
|
|
// DSReadyAt is when the DS can safely be added at the parent (phase
|
|
// signing).
|
|
DSReadyAt *time.Time `json:"ds_ready_at,omitempty"`
|
|
// CanPublishDS: the zone is ready for (or already has) a DS at the
|
|
// parent.
|
|
CanPublishDS bool `json:"can_publish_ds"`
|
|
// CanFinish: the zone is being turned off and the parent has no DS
|
|
// any more (as far as named knows), so DNSSEC can be switched off.
|
|
CanFinish bool `json:"can_finish"`
|
|
// NeedsDS lists the signing keys whose DS should be at the parent,
|
|
// and StaleDS those whose DS should be removed from it.
|
|
NeedsDS []uint16 `json:"needs_ds,omitempty"`
|
|
StaleDS []uint16 `json:"stale_ds,omitempty"`
|
|
}
|
|
|
|
// Assess works out the phase of a zone signed with policy ("" when Jarl
|
|
// doesn't sign it, "insecure" while signing is being removed) that has
|
|
// keys.
|
|
func Assess(policy string, keys []*Key) Phase {
|
|
var live []*Key // signing keys named keeps (or is introducing)
|
|
for _, k := range keys {
|
|
if k.SigningKey() && k.Goal == Omnipresent {
|
|
live = append(live, k)
|
|
}
|
|
}
|
|
switch {
|
|
case policy == "":
|
|
if len(keys) == 0 {
|
|
return Phase{Code: PhaseOff, Summary: "DNSSEC is off."}
|
|
}
|
|
return Phase{Code: PhaseLeftover, Summary: "DNSSEC is off, but keys from when the zone was signed are still in its key directory.",
|
|
Next: "Wipe them (jarl zone dnssec wipe) once you're sure the parent zone has no DS record for them, or turn signing on again to reuse them."}
|
|
case policy == "insecure":
|
|
return assessOff(keys)
|
|
}
|
|
if len(live) == 0 {
|
|
return Phase{Code: PhaseStarting, Summary: "Signing is on; named hasn't created the zone's keys yet.",
|
|
Next: "Wait a moment: named makes the keys when it loads the configuration."}
|
|
}
|
|
p := Phase{}
|
|
var notReady []*Key
|
|
for _, k := range live {
|
|
if k.DNSKEYState != Omnipresent || k.KeyRRSIG != Omnipresent {
|
|
notReady = append(notReady, k)
|
|
}
|
|
}
|
|
for _, k := range keys {
|
|
if k.SigningKey() && k.Goal == Hidden && k.DSState != Hidden && k.DSRemoved == nil {
|
|
p.StaleDS = append(p.StaleDS, k.Tag)
|
|
}
|
|
}
|
|
if len(notReady) > 0 {
|
|
p.Code = PhaseSigning
|
|
p.Summary = "named is signing the zone. Resolvers still treat it as unsigned, because the parent zone has no DS record for it."
|
|
for _, k := range notReady {
|
|
if k.PublishCDS != nil && (p.DSReadyAt == nil || k.PublishCDS.After(*p.DSReadyAt)) {
|
|
t := *k.PublishCDS
|
|
p.DSReadyAt = &t
|
|
}
|
|
}
|
|
if p.DSReadyAt != nil {
|
|
p.Next = fmt.Sprintf("Wait until %s before adding the DS at the parent: until then some resolvers may still have unsigned answers cached, and would reject them once the DS appears.", p.DSReadyAt.UTC().Format("Mon 2 Jan 15:04 MST"))
|
|
} else {
|
|
p.Next = "Wait until named has published the keys long enough for caches to pick them up before adding the DS."
|
|
}
|
|
return p
|
|
}
|
|
p.CanPublishDS = true
|
|
var pending, secure int
|
|
for _, k := range live {
|
|
switch {
|
|
case k.DSState == Omnipresent:
|
|
secure++
|
|
case k.DSPublished != nil:
|
|
pending++
|
|
default:
|
|
p.NeedsDS = append(p.NeedsDS, k.Tag)
|
|
}
|
|
}
|
|
switch {
|
|
case len(p.NeedsDS) > 0:
|
|
p.Code = PhaseReady
|
|
p.Summary = "The zone is signed and ready: add the DS record at the parent zone (your registrar) to turn validation on."
|
|
p.Next = "Add the DS record (" + tags(p.NeedsDS) + ") at your registrar, then run the DS check so named knows it's there."
|
|
case pending > 0:
|
|
p.Code = PhaseDSPending
|
|
p.Summary = "The parent zone has the DS record. named waits for the parent's TTL before it treats it as in place."
|
|
default:
|
|
p.Code = PhaseSecure
|
|
p.Summary = "The zone is signed and the parent zone's DS record points at its key: validating resolvers check its answers."
|
|
}
|
|
if len(p.StaleDS) > 0 && p.Next == "" {
|
|
p.Next = "Remove the DS record of the retired key " + tags(p.StaleDS) + " at your registrar, then run the DS check."
|
|
}
|
|
return p
|
|
}
|
|
|
|
func assessOff(keys []*Key) Phase {
|
|
var dsUp, dsGoing, keysLeft bool
|
|
for _, k := range keys {
|
|
if !k.SigningKey() {
|
|
if !k.Gone() {
|
|
keysLeft = true
|
|
}
|
|
continue
|
|
}
|
|
switch {
|
|
case k.DSState == Hidden || k.DSState == "":
|
|
case k.DSRemoved != nil:
|
|
dsGoing = true
|
|
default:
|
|
dsUp = true
|
|
}
|
|
if !k.Gone() {
|
|
keysLeft = true
|
|
}
|
|
}
|
|
switch {
|
|
case dsUp:
|
|
return Phase{Code: PhaseRemovingDS, Summary: "Turning DNSSEC off: named keeps the zone signed until the parent zone's DS record is gone.",
|
|
Next: "Remove the DS record at your registrar, then run the DS check so named knows. Don't switch DNSSEC off before that: the zone would fail to validate."}
|
|
case dsGoing:
|
|
return Phase{Code: PhaseDSExpiring, Summary: "Turning DNSSEC off: the parent's DS record is gone; named keeps signing until resolvers' cached copies of it expire (the parent's DS TTL, often a day).",
|
|
Next: "Wait: switching DNSSEC off now would make the zone fail to validate for resolvers that still have the DS cached."}
|
|
case keysLeft:
|
|
return Phase{Code: PhaseRemovingKeys, Summary: "Turning DNSSEC off: the parent has no DS any more, and named is removing the keys and signatures.",
|
|
Next: "Wait until named has finished, then switch DNSSEC off.", CanFinish: true}
|
|
}
|
|
return Phase{Code: PhaseUnsigned, Summary: "Turning DNSSEC off has finished: the zone is no longer signed.",
|
|
Next: "Switch DNSSEC off for the zone to finish (jarl zone dnssec disable), and wipe the old keys if you like.", CanFinish: true}
|
|
}
|
|
|
|
func tags(ts []uint16) string {
|
|
var s []string
|
|
for _, t := range ts {
|
|
s = append(s, fmt.Sprintf("key %d", t))
|
|
}
|
|
return strings.Join(s, ", ")
|
|
}
|