Let the generic command verb only read, and keep keys and tokens at the terminal

Review found a chain through the command verb: set the operator's key to one
the caller holds, rotate secrets so they are sealed to it too, read the sealed
copies, open them. Whoever may call a verb includes agents (hq ADR 0266), so
command now runs an allow list of reading forms, and operator, identity,
token, broker, api, licence and every secret command but rotate are refused
through any verb.
This commit is contained in:
jochen
2026-10-09 10:12:05 +02:00
parent 528951319c
commit 20d4f1ae71
7 changed files with 188 additions and 30 deletions
+66 -7
View File
@@ -2,6 +2,7 @@ package main
import (
"context"
"errors"
"fmt"
"github.com/novox/mesh-controller/internal/link"
"strings"
@@ -108,11 +109,14 @@ func TestRotateTakesAProvisionOrAnOwnSecret(t *testing.T) {
}
}
// `token` is `token issue` at a shell, with the machine's tunnel key (novox/hq ADR 0169).
func TestTokenIssuesForAMachineAndItsTunnelKey(t *testing.T) {
argv, err := argvFor("token", map[string]any{"new": "laptop", "overlay_key": "k", "for": "2h"})
if err != nil || strings.Join(argv, " ") != "token issue --new laptop --overlay-key k --for 2h" {
t.Fatalf("token: %v %v", argv, err)
// `token` answers a joining token to its caller, and whoever may call a verb includes agents: it is the
// controller's terminal's alone (novox/hq ADR 0266), refused through the verb whatever it is given.
func TestTokenIsRefusedThroughAVerb(t *testing.T) {
for _, args := range []map[string]any{{"new": "laptop", "overlay_key": "k", "for": "2h"}, {"node": "ace"}} {
argv, err := argvFor("token", args)
if err == nil || !strings.Contains(err.Error(), "controller's terminal only") {
t.Fatalf("token %v: %v %v", args, argv, err)
}
}
}
@@ -249,8 +253,8 @@ func TestCommandRunsTheLineAsGiven(t *testing.T) {
if err != nil || len(argv) != 6 || argv[3] != `{"a": "b c"}` {
t.Fatalf("a quoted word stays one word: %q %v", argv, err)
}
argv, err = argvFor("command", map[string]any{"command": `module show "the box" --json`})
if err != nil || len(argv) != 4 || argv[2] != "the box" {
argv, err = argvFor("command", map[string]any{"command": `plan "the box" --json`})
if err != nil || len(argv) != 3 || argv[1] != "the box" {
t.Fatalf("double quotes group: %q %v", argv, err)
}
if _, err := argvFor("command", map[string]any{"command": " "}); err == nil {
@@ -355,3 +359,58 @@ func TestTheControllerAnnouncesTheVerbsItServes(t *testing.T) {
}
}
}
// The generic verb only reads (novox/hq ADR 0266): an allow list of reading forms, and every line that
// writes, issues, sets a key or reveals a secret refused — the chain a review found ran through it: set the
// operator's key to one the caller holds, rotate secrets sealed to it, open them.
func TestTheCommandVerbOnlyReads(t *testing.T) {
for _, line := range []string{
"operator key set --replace k", "operator issue", "secret accept a b", "secret rotate a b c",
"secret recover a", "secret export a", "token issue --new x", "identity show", "broker users",
"api key", "licence show", "push anchor --why w", "assign novox m", "settings set m {}",
"settings clear m", "module add f", "module check /etc", "module forget m", "module issue m --node a",
"seat rename a b", "plans close p --why w", "plans go p", "doctor run", "conditions silence c --why w",
"retire approve x", "cleanup delete x", "delivery check", "delivery go x", "bus upgrade",
"mirrors --record x", "mirrors --confirm", "hand-act record x --why y --cause z", "serve", "migrate",
"prepare", "declare x", "overlay x", "facts", "merge-gate", "check-here", "build x", "rebuild x",
"cancel x", "kill x", "clear x", "replay x", "pause", "resume", "pin a b", "unpin a", "take x",
"converge", "adopt x", "rollout x", "upgrade x", "collect", "board", "builder", "ask x", "frobnicate",
} {
argv, err := argvFor("command", map[string]any{"command": line})
var policy *heldAtTheTerminal
if err == nil || !errors.As(err, &policy) {
t.Errorf("%q ran as %v (%v); the generic verb only reads", line, argv, err)
}
}
for _, line := range []string{
"status --json", "version", "seats --json", "healers", "hand-acts --days 3", "durations", "collection",
"images", "artifacts --collected", "data --machine a", "builds --log b", "queue", "plan ace --diff",
"plans", "plans plan-1", "doctor", "doctor probes", "doctor signals", "conditions", "conditions list",
"conditions show c", "conditions history", "node list", "node show ace", "module list",
"settings show m", "settings preferences", "retire list", "cleanup list", "delivery plan --repository r",
"delivery walks", "bus", "mirrors --json",
} {
if _, err := argvFor("command", map[string]any{"command": line}); err != nil {
t.Errorf("%q, a read, was refused: %v", line, err)
}
}
}
// What hands a caller a key, a credential or a secret is refused whichever verb composed it.
func TestNoVerbSetsTheOperatorsKeyOrRevealsASecret(t *testing.T) {
for _, argv := range [][]string{
{"operator", "key", "set"}, {"operator", "issue"}, {"identity"}, {"token", "issue"}, {"broker", "users"},
{"api"}, {"licence"}, {"secret", "export", "x"}, {"secret", "recover", "x"}, {"secret", "accept", "x"}, {"secret"},
} {
if err := terminalOnly(argv); err == nil {
t.Errorf("%v passed", argv)
}
}
if err := terminalOnly([]string{"secret", "rotate", "n", "m", "s"}); err != nil {
t.Errorf("rotating seals to the machine that uses the secret, and stays a verb's: %v", err)
}
// A policy refusal is not a verb this binary is behind on: every verb is still served.
if _, behind, err := seatToolHandlers(); err != nil || len(behind) != 0 {
t.Fatalf("behind %v: %v", behind, err)
}
}