From 76f3ca12b8b8eebcc25bb6647d2ed477ea9e1435 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 8 Oct 2026 20:52:44 +0200 Subject: [PATCH] Refuse to take over a system account as one that never becomes root The controller cannot read a machine's user database, so a name it is given for the agents' account (hq ADR 0266) may be a service's own; taking it over would hand agents that service's files. Refuse it, touching nothing. --- internal/apply/user.go | 15 +++++++++++++++ internal/apply/user_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/internal/apply/user.go b/internal/apply/user.go index 8926690..43ba427 100644 --- a/internal/apply/user.go +++ b/internal/apply/user.go @@ -21,6 +21,9 @@ import ( // are a package plus configuration **in somebody's home** — so a mesh with no notion of a user // can manage /etc and nothing anybody looks at. +// FirstLoginUID is the first uid of a person's login on the distributions the mesh runs on (login.defs UID_MIN). +const FirstLoginUID = 1000 + // applyUser makes a login match what was declared. // // Reconciling, like everything else here: it is not told whether the user is new. Creating, @@ -51,6 +54,18 @@ func applyUser(ctx context.Context, sys system.System, r *declaration.User, run return out, err } + // **An account that must never become root is never a system account taken over** (novox/hq ADR 0266). + // The controller cannot read this machine's user database, so it cannot tell that a name it was given is a + // service's own (postgres, a module's daemon). Taking one over as the agents' account would hand agents + // that service's files and rights. Refused before anything is touched. + if r.Root == declaration.RootNever && exists { + if uid, err := strconv.Atoi(login.UID); err != nil || uid < FirstLoginUID { + return out, fmt.Errorf("%q is declared as an account that never becomes root, and it already exists "+ + "here as a system account (uid %s, below %d): it is not taken over; name another account", + r.Name, login.UID, FirstLoginUID) + } + } + // **A shell is refused before anything is touched** (novox/hq issue 228). Refused after the // account was created or its groups changed, the account would be half the declaration's; a // refusal fails this resource and leaves the account exactly as it was. diff --git a/internal/apply/user_test.go b/internal/apply/user_test.go index e6e81a9..308268e 100644 --- a/internal/apply/user_test.go +++ b/internal/apply/user_test.go @@ -292,3 +292,32 @@ func TestTheFoundShellIsNotOverwrittenByASecondChange(t *testing.T) { t.Errorf("given back %q, not the shell from before the mesh", l.shells["operator"]) } } + +// An account declared never to become root is never a system account taken over (novox/hq ADR 0266): the +// controller cannot tell a service's account from a free name, so the node-engine refuses it, touching nothing. +func TestARootNeverAccountIsNotASystemAccountTakenOver(t *testing.T) { + l := &logins{shells: map[string]string{}} + run := func(ctx context.Context, name string, args ...string) (string, error) { + if name == "getent" && args[len(args)-1] == "postgres" { + l.asked = append(l.asked, name+" "+strings.Join(args, " ")) + return "postgres:x:70:70::/var/lib/postgres:/usr/bin/nologin\n", nil + } + return l.run(ctx, name, args...) + } + declared := func(name string) string { + return `{"declaration":1,"resources":[{"id":"claude-code.agent-account","type":"user","name":"` + name + `","root":"never"}]}` + } + report, _, err := Apply(context.Background(), archHost(t), parse(t, declared("postgres")), store.State{}, + store.OriginDeclared, run, nil, nil) + if err == nil || !strings.Contains(err.Error(), "system account") { + t.Fatalf("a system account is refused as the agents' account: %v %+v", err, report) + } + if l.did("usermod") || l.did("useradd") { + t.Fatalf("nothing is changed on the refused account: %v", l.asked) + } + l.shells["agent"] = "/bin/bash" // uid 1500 in the fake: a login + if _, _, err := Apply(context.Background(), archHost(t), parse(t, declared("agent")), store.State{}, + store.OriginDeclared, run, nil, nil); err != nil { + t.Fatalf("a login's account is taken: %v", err) + } +}