Give every view a TSIG key that selects it
Each view's match-clients starts with "key jarl-view-NAME" (after
"!key" for the views listed after it), so a query signed with a view's
key lands in that view from any address. The secrets are derived from a
random seed in Jarl's database. The delegation check signs its queries
to this server's own addresses with the key of the zone's view: before,
they got whichever view matched the server's address, usually internal.
jarl view key / GET /views/{id}/key show a key for dig -k (tsig:write,
audited). jarl-view- key names are reserved; the importer drops view
keys from a configuration Jarl wrote.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c696d02513
commit
384f6b3060
26 files changed
+463
-16
No files matched your search
@@ -14,6 +14,7 @@ import (
|
||||
"jarl/internal/dnsname"
|
||||
"jarl/internal/engine"
|
||||
"jarl/internal/model"
|
||||
"jarl/internal/render"
|
||||
"jarl/internal/rr"
|
||||
)
|
||||
|
||||
@@ -92,6 +93,31 @@ func (s *Server) getView(w http.ResponseWriter, r *http.Request, p *auth.Princip
|
||||
return nil
|
||||
}
|
||||
|
||||
// viewKey: GET /views/{id}/key is the TSIG key that selects the view
|
||||
// (render/viewkeys.go), with its secret: like revealing a TSIG key's, it
|
||||
// needs tsig:write and is audited.
|
||||
func (s *Server) viewKey(w http.ResponseWriter, r *http.Request, p *auth.Principal) error {
|
||||
st, err := s.state(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v, err := s.viewByPath(r, st, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := require(p, auth.Action{Resource: auth.ResTSIG, Write: true}); err != nil {
|
||||
return forbidden("showing a view's key needs tsig:write")
|
||||
}
|
||||
seed, err := engine.ViewKeySeed(r.Context(), s.store)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name := render.ViewKeyName(v.Name)
|
||||
s.eng.Audit(r.Context(), p, "tsig.reveal", "view", v.ID, v.Name, "success", "view key "+name+" shown")
|
||||
writeJSON(w, http.StatusOK, map[string]string{"name": name, "algorithm": render.ViewKeyAlgorithm, "secret": render.ViewKeySecret(seed, v.ID)})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) viewObject(r *http.Request) func(st *model.State, id int64) any {
|
||||
return func(st *model.State, id int64) any {
|
||||
if v := st.Views[id]; v != nil {
|
||||
|
||||
@@ -160,6 +160,9 @@ func (s *Server) zoneDelegation(w http.ResponseWriter, r *http.Request, p *auth.
|
||||
return badRequest("the delegation check compares the parent's delegation with the zone's NS records, so it needs a primary zone (this is a %s zone; check it where its primary is)", z.Type)
|
||||
}
|
||||
c := s.checker()
|
||||
// This server's own addresses are asked too: sign those queries with
|
||||
// the key of the zone's view, so they get that view's answer.
|
||||
c.LocalKey = s.eng.ViewKey(r.Context(), st, z)
|
||||
if v := r.URL.Query().Get("resolver"); v != "" {
|
||||
// An address only, always port 53: this is a DNS resolver.
|
||||
a, err := netip.ParseAddr(v)
|
||||
|
||||
@@ -52,6 +52,7 @@ func (s *Server) routes() {
|
||||
m.HandleFunc("POST "+p+"/views", s.authed(s.createView))
|
||||
m.HandleFunc("POST "+p+"/views/order", s.authed(s.reorderViews))
|
||||
m.HandleFunc("GET "+p+"/views/{id}", s.authed(s.getView))
|
||||
m.HandleFunc("GET "+p+"/views/{id}/key", s.authed(s.viewKey))
|
||||
m.HandleFunc("PATCH "+p+"/views/{id}", s.authed(s.updateView))
|
||||
m.HandleFunc("DELETE "+p+"/views/{id}", s.authed(s.deleteView))
|
||||
m.HandleFunc("PUT "+p+"/views/{id}/color", s.authed(s.setViewColor))
|
||||
|
||||
Reference in new issue
Block a user