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

192 lines
6.6 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")
// 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.*"] }
allow_responses: true } }
{ user: "node.ace", password: "pw", permissions: {
publish: { allow: ["mesh.seat.mesh-vault.call.node-key.node_ace"] }
subscribe: { allow: ["_INBOX.node.ace.>"] } } }
{ user: "node.g14", password: "pw", permissions: {
publish: { allow: ["mesh.seat.mesh-vault.call.node-key.node_g14"] }
subscribe: { allow: ["_INBOX.node.g14.>"] } } }
]
}
`
// 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)
tool := toolNamed(t, definitions(&Env{Vault: v.Vault}), Seat+".node-key")
vaultConn := connectAs(t, s.ClientURL(), "novox.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
}
aceKey, _ := 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)
}
}