Run the router as its own account, ask unlinking of the linked account, and take no approval where an agent can become root (hq ADR 0259 §7, §8)

This commit is contained in:
jochen
2026-10-09 17:08:17 +02:00
committed by jschoubben
parent c2c75ceb3b
commit debb897c21
20 changed files with 937 additions and 111 deletions
+89
View File
@@ -0,0 +1,89 @@
package main
// Who can become root here without a person (novox/hq ADR 0259 §8): the controller's self-check asks it of
// every machine where the router or a channel proving its sender runs, for the accounts agents run as and
// the account the machine's runtime runs commands as.
//
// Root without a person is any of: a sudo rule letting the account run every command as root with no
// password; membership of a group that is root by another name (docker, which mounts the host; disk, which
// reads it raw; wheel or sudo only where a rule grants them without a password, which the listing shows); or
// the account being root itself.
import (
"fmt"
"regexp"
"strings"
)
// Escalation is one account's answer.
type Escalation struct {
Account string `json:"account"`
Root bool `json:"root_without_a_person"`
Why string `json:"why"`
}
// rootGroups are groups that are root by another name.
var rootGroups = map[string]string{"docker": "the docker group, which can mount the machine's root file system",
"disk": "the disk group, which reads and writes the drives raw", "root": "the root group"}
var accountShape = regexp.MustCompile(`^[a-z_][a-z0-9_-]{0,31}$`)
// Escalations answers, for each account, whether it can become root here without a person.
func (m *Machine) Escalations(accounts []string) ([]Escalation, error) {
if len(accounts) == 0 {
return nil, fmt.Errorf("name the accounts to judge")
}
var out []Escalation
for _, a := range accounts {
if !accountShape.MatchString(a) {
return nil, fmt.Errorf("%q is not an account name", a)
}
out = append(out, m.escalation(a))
}
return out, nil
}
func (m *Machine) escalation(account string) Escalation {
e := Escalation{Account: account}
if account == "root" {
e.Root, e.Why = true, "it is root"
return e
}
id := m.Run(bg(), "id", "-nG", account)
if id.Status != 0 || id.Err != "" {
e.Why = "no such account here"
return e
}
for _, g := range strings.Fields(id.Stdout) {
if why, ok := rootGroups[g]; ok {
e.Root, e.Why = true, "a member of "+why
return e
}
}
// sudo's own word, for that account: listing another account's rules needs root, which the runtime's
// account escalates to as the mesh's acting tools do.
r := m.Run(bg(), "sudo", "-n", "-l", "-U", account)
if r.Status != 0 || r.Err != "" {
if strings.Contains(r.Stdout+r.Stderr, "not allowed to run sudo") {
e.Why = "no sudo rule names it"
return e
}
e.Why = "sudo could not be asked: " + strings.TrimSpace(firstLine(r.Stderr+r.Err))
return e
}
if strings.Contains(r.Stdout, "is not allowed to run sudo") {
e.Why = "no sudo rule names it"
return e
}
rules := ParseList(r.Stdout, account)
if rules.PasswordlessAll {
for _, rule := range rules.Rules {
if hasTag(rule.Tags, "NOPASSWD") && contains(rule.Commands, "ALL") && runsAsRoot(rule.RunAs) {
e.Root, e.Why = true, "sudo lets it run every command as root without a password ("+rule.Line+")"
return e
}
}
}
e.Why = "sudo asks it for a password, or lets it run named commands only"
return e
}