mesh/delivery closed: a newer head of the same pull request
mesh/check checking 1faf31706919 on 5b75233406fe | novox/mesh-catalog#505 in group feat/1126-nodes-compose
234 lines
9.0 KiB
Go
234 lines
9.0 KiB
Go
package main
|
|
|
|
// Issue 786 (ADR 0372): a machine's node-engine states its own sealing key to the vault, in its own name, at start and
|
|
// when it changes, whether or not it ever moved a secret in. The caller is the bus user the runtime proved; a changed
|
|
// key (or a first one) raises `secret-changed` for the machine's core secrets, the same key says nothing.
|
|
|
|
import (
|
|
"git.novox.be/novox/mesh-sdk/go/seal"
|
|
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/nats-io/nats-server/v2/server"
|
|
"github.com/nats-io/nats.go"
|
|
)
|
|
|
|
func changedNames(p *published) []string {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
var out []string
|
|
for _, e := range p.events {
|
|
if e["event"] == ChangedEvent {
|
|
out = append(out, e["body"].(map[string]any)["name"].(string))
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestANodeEngineStatesItsOwnKeyAndAChangeRaisesItsCoreSecrets(t *testing.T) {
|
|
v := newVault(t, nil)
|
|
tool := toolNamed(t, definitions(&Env{Vault: v.Vault}), Seat+".node-key")
|
|
// ace holds two core secrets and a leaf module's; g14 holds a core secret too.
|
|
old, _ := aSealingKey(t)
|
|
for _, m := range []MoveOf{
|
|
{Node: "ace", Module: "mesh-provisioner-client", Name: "provisioner-broker", Origin: "made", Generation: "g-1", SealingKey: old},
|
|
{Node: "ace", Module: "nats", Name: "broker", Origin: "made", Generation: "g-1"},
|
|
{Node: "ace", Module: "postgres", Name: "superuser-password", Origin: "made", Generation: "g-1"},
|
|
{Node: "g14", Module: "nats", Name: "broker", Origin: "made", Generation: "g-1"},
|
|
} {
|
|
if _, err := moveIn(t, v, "node."+m.Node, m, movedValue); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
// ace never stated a key in the moves above except through the first row; forget it to be a machine that never moved.
|
|
if err := os.Remove(v.Store.nodeKeyPath("ace")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
before := len(changedNames(v.bus))
|
|
|
|
pub, _ := aSealingKey(t)
|
|
if _, err := tool.runAs("controller", map[string]any{"key": pub}); err == nil {
|
|
t.Error("the controller stated a machine's key")
|
|
}
|
|
if _, err := tool.runAs("", map[string]any{"key": pub}); err == nil {
|
|
t.Error("an unproven caller stated a key")
|
|
}
|
|
if _, err := tool.runAs("node.ace", map[string]any{"key": "c2hvcnQ="}); err == nil {
|
|
t.Error("a short key was taken")
|
|
}
|
|
if _, err := tool.runAs("node.ace", map[string]any{"node": "g14", "key": pub}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// The machine is the caller's; an argument naming another one changes nothing.
|
|
if got, held, _ := v.Store.NodeKey("ace"); !held || got != pub {
|
|
t.Errorf("ace's key is %q, %v", got, held)
|
|
}
|
|
if _, held, _ := v.Store.NodeKey("g14"); held {
|
|
t.Error("a key was recorded for g14 by ace's engine")
|
|
}
|
|
got := changedNames(v.bus)[before:]
|
|
want := []string{"own/ace/mesh-provisioner-client/provisioner-broker", "own/ace/nats/broker"}
|
|
if strings.Join(got, ",") != strings.Join(want, ",") {
|
|
t.Errorf("secret-changed was raised for %v, want %v", got, want)
|
|
}
|
|
|
|
// The same key again changes nothing and says nothing.
|
|
if _, err := tool.runAs("node.ace", map[string]any{"key": pub}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n := len(changedNames(v.bus)); n != before+2 {
|
|
t.Errorf("the same key raised %d more", n-before-2)
|
|
}
|
|
// A different key raises them again.
|
|
other, _ := aSealingKey(t)
|
|
if _, err := tool.runAs("node.ace", map[string]any{"key": other}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n := len(changedNames(v.bus)); n != before+4 {
|
|
t.Errorf("a changed key raised %d, want 2", n-before-2)
|
|
}
|
|
if got, _, _ := v.Store.NodeKey("ace"); got != other {
|
|
t.Error("the changed key was not recorded")
|
|
}
|
|
}
|
|
|
|
const nodeKeyBus = `listen: 127.0.0.1:-1
|
|
authorization {
|
|
users = [
|
|
{ user: "novox.mesh-vault", password: "pw", permissions: {
|
|
publish: { deny: [">"] }
|
|
subscribe: { allow: ["mesh.seat.mesh-vault.call.node-key.*", "mesh.seat.mesh-vault.call.own.*"] }
|
|
allow_responses: true } }
|
|
{ user: "node.ace", password: "pw", permissions: {
|
|
publish: { allow: ["mesh.seat.mesh-vault.call.node-key.node_ace", "mesh.seat.mesh-vault.call.own.node_ace"] }
|
|
subscribe: { allow: ["_INBOX.node.ace.>"] } } }
|
|
{ user: "node.g14", password: "pw", permissions: {
|
|
publish: { allow: ["mesh.seat.mesh-vault.call.node-key.node_g14", "mesh.seat.mesh-vault.call.own.node_g14"] }
|
|
subscribe: { allow: ["_INBOX.node.g14.>"] } } }
|
|
{ user: "controller", password: "pw", permissions: {
|
|
publish: { allow: ["mesh.seat.mesh-vault.call.own.controller"] }
|
|
subscribe: { allow: ["_INBOX.controller.>"] } } }
|
|
]
|
|
}
|
|
`
|
|
|
|
// On a real bus whose accounts may publish only their own subject: node.ace's statement records ace's key; node.g14
|
|
// cannot speak in ace's name, and what it states is g14's.
|
|
func TestOnARealBusANodeEngineStatesOnlyItsOwnKey(t *testing.T) {
|
|
file := filepath.Join(t.TempDir(), "bus.conf")
|
|
if err := os.WriteFile(file, []byte(nodeKeyBus), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
opts, err := server.ProcessConfigFile(file)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
opts.NoLog, opts.NoSigs = true, true
|
|
s, err := server.NewServer(opts)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
go s.Start()
|
|
if !s.ReadyForConnections(10 * time.Second) {
|
|
t.Fatal("the bus did not come up")
|
|
}
|
|
t.Cleanup(s.Shutdown)
|
|
|
|
v := newVault(t, nil)
|
|
v.Askers = Declared{Read: assignments{"ace.web": `{"node":"ace","module":"web"}`}.read, Store: v.Store}
|
|
tools := map[string]toolDef{"node-key": toolNamed(t, definitions(&Env{Vault: v.Vault}), Seat+".node-key"),
|
|
"own": toolNamed(t, definitions(&Env{Vault: v.Vault}), Seat+".own")}
|
|
vaultConn := connectAs(t, s.ClientURL(), "novox.mesh-vault")
|
|
serve := func(m *nats.Msg) {
|
|
parts := strings.Split(m.Subject, ".")
|
|
var args map[string]any
|
|
_ = json.Unmarshal(m.Data, &args)
|
|
answer := busAnswer{}
|
|
if got, err := tools[parts[len(parts)-2]].runAs(untoken(parts[len(parts)-1]), args); err != nil {
|
|
answer.Error = err.Error()
|
|
} else {
|
|
answer.Result, _ = json.Marshal(got)
|
|
}
|
|
raw, _ := json.Marshal(answer)
|
|
_ = m.Respond(raw)
|
|
}
|
|
for _, verb := range []string{"node-key", "own"} {
|
|
if _, err := vaultConn.Subscribe("mesh.seat.mesh-vault.call."+verb+".*", serve); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
_ = vaultConn.Flush()
|
|
|
|
call := func(user, subject string, args any) (json.RawMessage, error) {
|
|
raw, _ := json.Marshal(args)
|
|
msg, err := connectAs(t, s.ClientURL(), user).Request(subject, raw, 3*time.Second)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var a busAnswer
|
|
_ = json.Unmarshal(msg.Data, &a)
|
|
if a.Error != "" {
|
|
return nil, errString(a.Error)
|
|
}
|
|
return a.Result, nil
|
|
}
|
|
say := func(user, subject, key string) (string, error) {
|
|
_, err := call(user, subject, map[string]string{"key": key})
|
|
return "", err
|
|
}
|
|
aceKey, acePriv := aSealingKey(t)
|
|
g14Key, _ := aSealingKey(t)
|
|
if _, err := say("node.ace", "mesh.seat.mesh-vault.call.node-key.node_ace", aceKey); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, held, _ := v.Store.NodeKey("ace"); !held || got != aceKey {
|
|
t.Errorf("ace's key is %q, %v", got, held)
|
|
}
|
|
// g14 may not publish on ace's subject: the bus refuses it, so the vault never hears it.
|
|
if _, err := say("node.g14", "mesh.seat.mesh-vault.call.node-key.node_ace", g14Key); err == nil {
|
|
t.Error("node.g14 spoke in ace's name")
|
|
}
|
|
if got, _, _ := v.Store.NodeKey("ace"); got != aceKey {
|
|
t.Error("ace's key was replaced by g14's")
|
|
}
|
|
if _, err := say("node.g14", "mesh.seat.mesh-vault.call.node-key.node_g14", g14Key); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, held, _ := v.Store.NodeKey("g14"); !held || got != g14Key {
|
|
t.Errorf("g14's key is %q, %v", got, held)
|
|
}
|
|
|
|
// Issue 1126 (ADR 0547 §2, ADR 0550 §5): `own` on the same bus. node.ace asks its own and gets its module's secret
|
|
// sealed to the key it stated, opening with its key alone; g14 cannot ask in ace's name, and asking in its own gets
|
|
// none of ace's; the controller is refused, as no node-engine.
|
|
if _, err := moveIn(t, v, "node.ace", MoveOf{Node: "ace", Module: "web", Name: "password", Origin: "made", Generation: "g-1",
|
|
SealingKey: aceKey}, "ace-web-pw"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw, err := call("node.ace", "mesh.seat.mesh-vault.call.own.node_ace", map[string]any{})
|
|
var own OwnAnswer
|
|
if err != nil || json.Unmarshal(raw, &own) != nil || own.Node != "ace" || len(own.Own) != 1 {
|
|
t.Fatalf("ace's own over the bus: %s, %v", raw, err)
|
|
}
|
|
if value, err := seal.Open(own.Own[0].Sealed, acePriv); err != nil || string(value) != "ace-web-pw" {
|
|
t.Errorf("ace's secret does not open with ace's key: %q, %v", value, err)
|
|
}
|
|
if strings.Contains(string(raw), "ace-web-pw") {
|
|
t.Error("ace's secret crossed the bus in the clear")
|
|
}
|
|
if _, err := call("node.g14", "mesh.seat.mesh-vault.call.own.node_ace", map[string]any{}); err == nil {
|
|
t.Error("node.g14 asked ace's own in ace's name")
|
|
}
|
|
if raw, err := call("node.g14", "mesh.seat.mesh-vault.call.own.node_g14", map[string]any{}); err != nil || strings.Contains(string(raw), "web") {
|
|
t.Errorf("g14 asking its own was answered %s, %v", raw, err)
|
|
}
|
|
if _, err := call("controller", "mesh.seat.mesh-vault.call.own.controller", map[string]any{}); err == nil || !strings.Contains(err.Error(), "not a node-engine") {
|
|
t.Errorf("the controller asking own: %v", err)
|
|
}
|
|
}
|