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
@@ -149,7 +149,7 @@ const headerText = "Generated by Jarl. DO NOT EDIT: changes made here are overwr
|
||||
// from jarld's configuration (e.g. for query statistics).
|
||||
func Render(s *model.State, p Paths, x Extras) *Output {
|
||||
out := &Output{Spans: map[string][]Span{}, ZoneFiles: map[int64]string{}}
|
||||
keys := renderKeys(s)
|
||||
keys := renderKeys(s) + renderViewKeys(s, x)
|
||||
out.Files = append(out.Files, &File{Path: p.KeysConfig(), Content: []byte(keys), Mode: 0o640, Kind: KindKeys, Secret: true})
|
||||
conf, spans := renderConfig(s, p, x, out)
|
||||
out.Files = append(out.Files, &File{Path: p.MainConfig(), Content: []byte(conf), Mode: 0o640, Kind: KindConfig})
|
||||
@@ -350,8 +350,8 @@ func renderConfig(s *model.State, p Paths, x Extras, out *Output) (string, []Spa
|
||||
w.write(commentBlock("//", v.Comment))
|
||||
}
|
||||
w.write(fmt.Sprintf("view %s {\n", namedconf.QuoteString(v.Name)))
|
||||
if len(v.MatchClients) > 0 {
|
||||
writeAML(w, "match-clients", v.MatchClients)
|
||||
if mc := viewMatchClients(s, x, v); len(mc) > 0 {
|
||||
writeAML(w, "match-clients", mc)
|
||||
}
|
||||
if len(v.MatchDestinations) > 0 {
|
||||
writeAML(w, "match-destinations", v.MatchDestinations)
|
||||
|
||||
@@ -15,6 +15,9 @@ type Extras struct {
|
||||
// Stats, when set, makes named publish what Jarl's statistics
|
||||
// collector reads.
|
||||
Stats *StatsHooks
|
||||
// ViewKeySeed, when set, gives every view a TSIG key that selects it
|
||||
// (viewkeys.go).
|
||||
ViewKeySeed []byte
|
||||
}
|
||||
|
||||
// StatsHooks describe the statistics channel and response log Jarl needs.
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"jarl/internal/model"
|
||||
"jarl/internal/namedconf"
|
||||
)
|
||||
|
||||
// View keys let Jarl choose a view regardless of where a query comes from:
|
||||
// each view gets a TSIG key, and its match-clients starts with that key
|
||||
// (after excluding the keys of the views listed after it, so no earlier
|
||||
// view takes their queries). Queries signed with a view's key always land
|
||||
// in that view. Jarl signs its own checks of this server's addresses with
|
||||
// them (a query from this host would otherwise land in whichever view
|
||||
// matches this host's address), and you can too: dig -k.
|
||||
//
|
||||
// The secrets are derived from a random seed kept in Jarl's database
|
||||
// (Extras.ViewKeySeed), so they stay the same from one apply to the next
|
||||
// and need no storage of their own. They are in keys.conf with the other
|
||||
// keys.
|
||||
|
||||
// ViewKeyAlgorithm is the view keys' algorithm.
|
||||
const ViewKeyAlgorithm = "hmac-sha256"
|
||||
|
||||
// ViewKeyName returns the name of a view's key.
|
||||
func ViewKeyName(view string) string { return model.ViewKeyName(view) }
|
||||
|
||||
// IsViewKey reports whether a key name is a view key's.
|
||||
func IsViewKey(name string) bool {
|
||||
return strings.HasPrefix(strings.ToLower(strings.TrimSuffix(name, ".")), model.ViewKeyPrefix)
|
||||
}
|
||||
|
||||
// ViewKeySecret returns the secret of the key of the view with this ID.
|
||||
func ViewKeySecret(seed []byte, viewID int64) string {
|
||||
m := hmac.New(sha256.New, seed)
|
||||
m.Write([]byte("jarl view key " + strconv.FormatInt(viewID, 10)))
|
||||
return base64.StdEncoding.EncodeToString(m.Sum(nil))
|
||||
}
|
||||
|
||||
// viewKeysOn reports whether view keys are rendered.
|
||||
func viewKeysOn(s *model.State, x Extras) bool { return len(x.ViewKeySeed) > 0 && len(s.Views) > 0 }
|
||||
|
||||
// renderViewKeys returns the key statements of the views' keys.
|
||||
func renderViewKeys(s *model.State, x Extras) string {
|
||||
if !viewKeysOn(s, x) {
|
||||
return ""
|
||||
}
|
||||
var sb strings.Builder
|
||||
sb.WriteString("\n" + commentBlock("//", "View keys: a query signed with one of these always lands in its view.\nJarl signs its own checks of this server with them; dig -k works too."))
|
||||
for _, v := range s.OrderedViews() {
|
||||
sb.WriteString("key " + namedconf.QuoteString(ViewKeyName(v.Name)) + " {\n\talgorithm " + ViewKeyAlgorithm + ";\n\tsecret " + namedconf.QuoteString(ViewKeySecret(x.ViewKeySeed, v.ID)) + ";\n};\n")
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// viewMatchClients returns a view's match-clients with the view keys in
|
||||
// front: "!key" for the keys of the views after it, then its own key, then
|
||||
// the view's own list ("any" when it has none, which is what an absent
|
||||
// match-clients means).
|
||||
func viewMatchClients(s *model.State, x Extras, v *model.View) []string {
|
||||
if !viewKeysOn(s, x) {
|
||||
return v.MatchClients
|
||||
}
|
||||
var out []string
|
||||
after := false
|
||||
for _, o := range s.OrderedViews() {
|
||||
if after {
|
||||
out = append(out, "!key "+namedconf.QuoteString(ViewKeyName(o.Name)))
|
||||
}
|
||||
if o.ID == v.ID {
|
||||
after = true
|
||||
}
|
||||
}
|
||||
out = append(out, "key "+namedconf.QuoteString(ViewKeyName(v.Name)))
|
||||
if len(v.MatchClients) == 0 {
|
||||
return append(out, "any")
|
||||
}
|
||||
return append(out, v.MatchClients...)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"jarl/internal/model"
|
||||
)
|
||||
|
||||
func TestViewKeys(t *testing.T) {
|
||||
s := model.NewState()
|
||||
s.Views[1] = &model.View{ID: 1, Name: "internal", Position: 0, MatchClients: []string{"homelab", "localhost"}}
|
||||
s.Views[2] = &model.View{ID: 2, Name: "external", Position: 1}
|
||||
p := Paths{ConfigDir: "/etc/bind/jarl", ZoneDir: "/var/lib/bind/jarl", CacheDir: "/var/cache/bind/jarl"}
|
||||
seed := []byte("0123456789abcdef0123456789abcdef")
|
||||
files := func(x Extras) (conf, keys string) {
|
||||
for _, f := range Render(s, p, x).Files {
|
||||
switch f.Kind {
|
||||
case KindConfig:
|
||||
conf = string(f.Content)
|
||||
case KindKeys:
|
||||
keys = string(f.Content)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
conf, keys := files(Extras{ViewKeySeed: seed})
|
||||
for _, want := range []string{
|
||||
// An earlier view mustn't take a later view's key.
|
||||
"match-clients {\n\t\t!key jarl-view-external;\n\t\tkey jarl-view-internal;\n\t\thomelab;\n\t\tlocalhost;\n\t};",
|
||||
// A view without a list matched everyone: it still does.
|
||||
"match-clients { key jarl-view-external; any; };",
|
||||
} {
|
||||
if !strings.Contains(conf, want) {
|
||||
t.Errorf("named.conf lacks\n%s\n---\n%s", want, conf)
|
||||
}
|
||||
}
|
||||
sec := ViewKeySecret(seed, 2)
|
||||
if !strings.Contains(keys, "key \"jarl-view-external\" {\n\talgorithm hmac-sha256;\n\tsecret \""+sec+"\";\n};") || !strings.Contains(keys, "jarl-view-internal") {
|
||||
t.Errorf("keys.conf:\n%s", keys)
|
||||
}
|
||||
// The same seed gives the same secret; another seed doesn't.
|
||||
if ViewKeySecret(seed, 2) != sec || ViewKeySecret([]byte("other"), 2) == sec || ViewKeySecret(seed, 1) == sec {
|
||||
t.Error("secrets aren't derived from the seed and the view")
|
||||
}
|
||||
// Without a seed (or without views), nothing changes.
|
||||
conf, keys = files(Extras{})
|
||||
if strings.Contains(conf, "jarl-view-") || strings.Contains(keys, "jarl-view-") {
|
||||
t.Errorf("view keys without a seed:\n%s", conf)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user