Refuse a node's accounts through any verb, and a stale or service-account agent verdict

The generic command verb ran node account and node agent-account, so an agent
could name itself the operator account and have the next send grant it root
(hq ADR 0266 review). Refuse every node subcommand but list and show through
any verb; refuse the operator account as the agent account in both
directions and well-known service accounts as an agent account; and count a
verdict heard more than 15 minutes ago as not judged, so stopping the
node-engine cannot freeze a healthy one. Re-pin mesh-host to its review head.
This commit is contained in:
jochen
2026-10-09 10:11:21 +02:00
parent fcfbf7e69e
commit dcbbf4487a
10 changed files with 155 additions and 18 deletions
+16 -4
View File
@@ -27,6 +27,7 @@ import (
"fmt"
"sort"
"strings"
"time"
"github.com/novox/mesh-controller/internal/conditions"
"github.com/novox/mesh-controller/internal/inventory"
@@ -60,16 +61,27 @@ func agentConfined(ctx context.Context, inv *inventory.Inventory, node string) (
if err != nil {
return true, false, "", err
}
confined, why = judgedConfined(n.AgentAccount, h, had)
confined, why = judgedConfined(n.AgentAccount, h, had, time.Now())
return true, confined, why, nil
}
// judgedConfined is the judgement over one statement, without the store.
func judgedConfined(agent string, h inventory.NodeHealth, had bool) (bool, string) {
// verdictFreshFor is how old the statement holding the verdict may be, by this controller's clock. A
// node-engine states its health on every change and at least every five minutes (mesh-host's sayAnyway), so
// three statements missed is a node-engine stopped, or a machine away. **A stale verdict is not a pass**: an
// agent that stopped the node-engine must not leave "cannot become root" standing from before.
const verdictFreshFor = 15 * time.Minute
// judgedConfined is the judgement over one statement, without the store, at now.
func judgedConfined(agent string, h inventory.NodeHealth, had bool, now time.Time) (bool, string) {
if !had {
return false, fmt.Sprintf("the agent account %s is not judged: the machine's node-engine has stated "+
"nothing of what it runs", agent)
}
if age := now.Sub(h.HeardAt); age > verdictFreshFor {
return false, fmt.Sprintf("the agent account %s is not judged: the machine's newest statement was heard at "+
"%s, more than %d minutes ago, and a verdict that old is not a verdict on now", agent,
h.HeardAt.Local().Format("2006-01-02 15:04"), int(verdictFreshFor.Minutes()))
}
if h.Contract < link.RootContract {
return false, fmt.Sprintf("the agent account %s is not judged: the machine's node-engine is older than "+
"the judging of an account's root (its statement's contract is %d, the judging is %d)",
@@ -122,7 +134,7 @@ func probeAgentAccounts(ctx context.Context, d *doctor) ([]conditions.Observatio
if err != nil {
return nil, err
}
confined, why := judgedConfined(n.AgentAccount, h, had)
confined, why := judgedConfined(n.AgentAccount, h, had, time.Now())
if confined {
continue
}