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
+40
-11
@@ -122,6 +122,8 @@ type ApplyOptions struct {
|
||||
Force bool
|
||||
// NoReload writes the files without reloading named.
|
||||
NoReload bool
|
||||
// wipeZone lets WipeDNSSEC switch signing off for this zone at once.
|
||||
wipeZone int64
|
||||
}
|
||||
|
||||
func (e *Engine) replay(base *model.State, ops []model.Op, p *auth.Principal) (*model.State, []string, error) {
|
||||
@@ -397,11 +399,11 @@ func (pl *plan) files() []string {
|
||||
return out
|
||||
}
|
||||
|
||||
func (e *Engine) makePlan(ctx context.Context, base, work *model.State, user string) (*plan, error) {
|
||||
func (e *Engine) makePlan(ctx context.Context, base, work *model.State, user string, opts ApplyOptions) (*plan, error) {
|
||||
pl := &plan{base: base, work: work, verify: map[int64]uint32{}}
|
||||
before := e.validate(base)
|
||||
after := e.validate(work)
|
||||
pl.introduced = validate.Introduced(before, after)
|
||||
pl.introduced = append(validate.Introduced(before, after), e.dnssecChanges(base, work, opts)...)
|
||||
introducedKeys := map[string]bool{}
|
||||
for _, p := range pl.introduced {
|
||||
introducedKeys[p.Key()] = true
|
||||
@@ -415,6 +417,12 @@ func (e *Engine) makePlan(ctx context.Context, base, work *model.State, user str
|
||||
return pl, nil
|
||||
}
|
||||
pl.serials = bumpSerials(base, work, e.Now(), user)
|
||||
guarded, problems := e.guardSerials(ctx, base, work, pl.serials, user)
|
||||
pl.serials = append(pl.serials, guarded...)
|
||||
if validate.HasErrors(problems) {
|
||||
pl.introduced = append(pl.introduced, problems...)
|
||||
return pl, nil
|
||||
}
|
||||
pl.out = render.Render(work, e.cfg.Paths, e.cfg.Extras)
|
||||
deployed, err := e.store.DeployedFiles(ctx)
|
||||
if err != nil {
|
||||
@@ -498,6 +506,11 @@ func (e *Engine) stagingCheck(ctx context.Context, pl *plan) error {
|
||||
defer os.RemoveAll(dir)
|
||||
sp := render.Paths{ConfigDir: filepath.Join(dir, "config"), ZoneDir: filepath.Join(dir, "zones"), CacheDir: filepath.Join(dir, "cache")}
|
||||
sout := render.Render(pl.work, sp, e.cfg.Extras)
|
||||
for _, d := range sout.Dirs {
|
||||
if err := os.MkdirAll(d, 0o750); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, f := range sout.Files {
|
||||
if err := os.MkdirAll(filepath.Dir(f.Path), 0o750); err != nil {
|
||||
return err
|
||||
@@ -635,7 +648,7 @@ func (e *Engine) Preview(ctx context.Context, csID int64, p *auth.Principal) (*R
|
||||
res.Error = ee
|
||||
return res, nil
|
||||
}
|
||||
pl, err := e.makePlan(ctx, base, work, p.Name())
|
||||
pl, err := e.makePlan(ctx, base, work, p.Name(), ApplyOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -714,7 +727,7 @@ func (e *Engine) applyOps(ctx context.Context, cs *model.Changeset, reqs []Reque
|
||||
cs.Description += fmt.Sprintf(" (+%d more)", len(cs.Ops)-1)
|
||||
}
|
||||
}
|
||||
pl, err := e.makePlan(ctx, base, work, p.Name())
|
||||
pl, err := e.makePlan(ctx, base, work, p.Name(), opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -782,6 +795,7 @@ func (e *Engine) applyOps(ctx context.Context, cs *model.Changeset, reqs []Reque
|
||||
}
|
||||
res.ChangesetID = cs.ID
|
||||
res.Applied = true
|
||||
e.removeDerived(pl)
|
||||
e.log.Info("changeset applied", "changeset", cs.ID, "user", p.Name(), "ops", len(cs.Ops), "files", len(res.Files), "description", cs.Description)
|
||||
return res, nil
|
||||
}
|
||||
@@ -956,15 +970,16 @@ func (e *Engine) Resync(ctx context.Context, p *auth.Principal, opts ApplyOption
|
||||
return nil, err
|
||||
}
|
||||
work := base.Clone()
|
||||
pl, err := e.makePlan(ctx, base, work, p.Name())
|
||||
pl, err := e.makePlan(ctx, base, work, p.Name(), opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := pl.result()
|
||||
res.DryRun = opts.DryRun
|
||||
if validate.HasErrors(pl.existing) {
|
||||
if validate.HasErrors(pl.existing) || validate.HasErrors(pl.introduced) {
|
||||
// Resync deploys the stored state as is; it must be valid.
|
||||
return res, &Error{Kind: KindValidation, Message: "the stored configuration has errors: " + describeErrors(pl.existing), Problems: validate.Errors(pl.existing)}
|
||||
ps := append(append([]validate.Problem{}, pl.existing...), pl.introduced...)
|
||||
return res, &Error{Kind: KindValidation, Message: "the stored configuration has errors: " + describeErrors(ps), Problems: validate.Errors(ps)}
|
||||
}
|
||||
if pl.check != nil && !pl.check.OK {
|
||||
return res, &Error{Kind: KindValidation, Message: "named-checkconf rejected the configuration: " + firstCheckError(pl.checkMsgs), Check: pl.checkMsgs}
|
||||
@@ -998,15 +1013,26 @@ func (e *Engine) Resync(ctx context.Context, p *auth.Principal, opts ApplyOption
|
||||
}
|
||||
deployed := deployedList(pl.out, 0, e.Now())
|
||||
err = e.store.InTx(ctx, func(tx *store.Tx) error {
|
||||
// Serials raised above named's (guardSerials) are kept.
|
||||
if len(pl.serials) > 0 {
|
||||
if err := tx.SaveState(base, work); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := tx.SetDeployedFiles(deployed); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.AddAudit(e.auditEvent(p, "system.resync", "", 0, "", 0, "success", fmt.Sprintf("%d files written", len(res.Files)), nil))
|
||||
var details json.RawMessage
|
||||
if len(pl.serials) > 0 {
|
||||
details, _ = json.Marshal(map[string]any{"serials": pl.serials})
|
||||
}
|
||||
return tx.AddAudit(e.auditEvent(p, "system.resync", "", 0, "", 0, "success", fmt.Sprintf("%d files written", len(res.Files)), details))
|
||||
})
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
res.Applied = true
|
||||
e.removeDerived(pl)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
@@ -1058,7 +1084,7 @@ func (e *Engine) CheckCurrent(ctx context.Context) (*Result, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pl, err := e.makePlan(ctx, base, base.Clone(), "")
|
||||
pl, err := e.makePlan(ctx, base, base.Clone(), "", ApplyOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1192,7 +1218,9 @@ func (e *Engine) WorkingState(ctx context.Context, csID int64, p *auth.Principal
|
||||
}
|
||||
|
||||
// LoadedSerial returns the serial named serves for zone z, from rndc
|
||||
// zonestatus. ok is false when named isn't asked (no_reload) or doesn't
|
||||
// zonestatus: for a zone named signs, the signed zone's serial (which
|
||||
// secondaries and the rest of the world see), not that of the zone file
|
||||
// Jarl wrote. ok is false when named isn't asked (no_reload) or doesn't
|
||||
// report a serial (the zone isn't loaded yet).
|
||||
func (e *Engine) LoadedSerial(ctx context.Context, st *model.State, z *model.Zone) (serial uint32, ok bool, err error) {
|
||||
if e.cfg.NoReload || e.tools == nil {
|
||||
@@ -1202,5 +1230,6 @@ func (e *Engine) LoadedSerial(ctx context.Context, st *model.State, z *model.Zon
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
return zs.Serial, zs.HasSerial, nil
|
||||
serial, ok = zs.ServedSerial()
|
||||
return serial, ok, nil
|
||||
}
|
||||
Reference in new issue
Block a user