192 lines
6.6 KiB
Go
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)
|
|
}
|
|
}
|