Files
mesh-catalog/modules/mesh-vault/cmd/mesh-vault/nodekey_test.go
T

192 lines
6.8 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 (
"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")
// node-a holds two core secrets and a leaf module's; node-b holds a core secret too.
old, _ := aSealingKey(t)
for _, m := range []MoveOf{
{Node: "node-a", Module: "mesh-provisioner-client", Name: "provisioner-broker", Origin: "made", Generation: "g-1", SealingKey: old},
{Node: "node-a", Module: "nats", Name: "broker", Origin: "made", Generation: "g-1"},
{Node: "node-a", Module: "postgres", Name: "superuser-password", Origin: "made", Generation: "g-1"},
{Node: "node-b", Module: "nats", Name: "broker", Origin: "made", Generation: "g-1"},
} {
if _, err := moveIn(t, v, "node."+m.Node, m, movedValue); err != nil {
t.Fatal(err)
}
}
// node-a 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("node-a")); 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.node-a", map[string]any{"key": "c2hvcnQ="}); err == nil {
t.Error("a short key was taken")
}
if _, err := tool.runAs("node.node-a", map[string]any{"node": "node-b", "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("node-a"); !held || got != pub {
t.Errorf("node-a's key is %q, %v", got, held)
}
if _, held, _ := v.Store.NodeKey("node-b"); held {
t.Error("a key was recorded for node-b by node-a's engine")
}
got := changedNames(v.bus)[before:]
want := []string{"own/node-a/mesh-provisioner-client/provisioner-broker", "own/node-a/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.node-a", 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.node-a", 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("node-a"); got != other {
t.Error("the changed key was not recorded")
}
}
const nodeKeyBus = `listen: 127.0.0.1:-1
authorization {
users = [
{ user: "node-c.mesh-vault", password: "pw", permissions: {
publish: { deny: [">"] }
subscribe: { allow: ["mesh.seat.mesh-vault.call.node-key.*"] }
allow_responses: true } }
{ user: "node.node-a", password: "pw", permissions: {
publish: { allow: ["mesh.seat.mesh-vault.call.node-key.node_node-a"] }
subscribe: { allow: ["_INBOX.node.node-a.>"] } } }
{ user: "node.node-b", password: "pw", permissions: {
publish: { allow: ["mesh.seat.mesh-vault.call.node-key.node_node-b"] }
subscribe: { allow: ["_INBOX.node.node-b.>"] } } }
]
}
`
// On a real bus whose accounts may publish only their own subject: node.node-a's statement records node-a's key; node.node-b
// cannot speak in node-a's name, and what it states is node-b'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)
tool := toolNamed(t, definitions(&Env{Vault: v.Vault}), Seat+".node-key")
vaultConn := connectAs(t, s.ClientURL(), "node-c.mesh-vault")
if _, err := vaultConn.Subscribe("mesh.seat.mesh-vault.call.node-key.*", func(m *nats.Msg) {
parts := strings.Split(m.Subject, ".")
var args map[string]any
_ = json.Unmarshal(m.Data, &args)
answer := busAnswer{}
if got, err := tool.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)
}); err != nil {
t.Fatal(err)
}
_ = vaultConn.Flush()
say := func(user, subject, key string) (string, error) {
raw, _ := json.Marshal(map[string]string{"key": key})
msg, err := connectAs(t, s.ClientURL(), user).Request(subject, raw, 3*time.Second)
if err != nil {
return "", err
}
var a busAnswer
_ = json.Unmarshal(msg.Data, &a)
if a.Error != "" {
return a.Error, errString(a.Error)
}
return "", nil
}
nodeAKey, _ := aSealingKey(t)
nodeBKey, _ := aSealingKey(t)
if _, err := say("node.node-a", "mesh.seat.mesh-vault.call.node-key.node_node-a", nodeAKey); err != nil {
t.Fatal(err)
}
if got, held, _ := v.Store.NodeKey("node-a"); !held || got != nodeAKey {
t.Errorf("node-a's key is %q, %v", got, held)
}
// node-b may not publish on node-a's subject: the bus refuses it, so the vault never hears it.
if _, err := say("node.node-b", "mesh.seat.mesh-vault.call.node-key.node_node-a", nodeBKey); err == nil {
t.Error("node.node-b spoke in node-a's name")
}
if got, _, _ := v.Store.NodeKey("node-a"); got != nodeAKey {
t.Error("node-a's key was replaced by node-b's")
}
if _, err := say("node.node-b", "mesh.seat.mesh-vault.call.node-key.node_node-b", nodeBKey); err != nil {
t.Fatal(err)
}
if got, held, _ := v.Store.NodeKey("node-b"); !held || got != nodeBKey {
t.Errorf("node-b's key is %q, %v", got, held)
}
}