diff --git a/go/catalogue/process_bus.go b/go/catalogue/process_bus.go index 3477691..25dea20 100644 --- a/go/catalogue/process_bus.go +++ b/go/catalogue/process_bus.go @@ -97,8 +97,8 @@ func ToolsBusSecret(m Manifest, name string) bool { } // toolsSecretProblems refuses a tools bundle's bus account secret the runtime's account could not read: it is declared -// as an own secret `broker-`, at a path in a directory resource of the module that others may pass through -// (mode with its last digit odd, such as 0711 or 0755); the file itself is 0600 and the runtime account's. +// as an own secret `broker-`, at `${dir:}/broker-` for a directory resource every parent of which the +// runtime's account may pass (passableDir); the file itself is 0600 and the runtime account's. func toolsSecretProblems(m Manifest) []string { var problems []string for _, a := range m.BusAccounts() { @@ -113,24 +113,57 @@ func toolsSecretProblems(m Manifest) []string { continue } dir, _, _ := strings.Cut(strings.TrimPrefix(own.Path, "${dir:"), "}") - passable := false - for _, r := range m.Resources { - if fmt.Sprint(r["type"]) != "directory" || fmt.Sprint(r["id"]) != dir || !strings.HasPrefix(own.Path, "${dir:"+dir+"}/") { - continue - } - mode, _ := r["mode"].(string) - passable = mode != "" && strings.ContainsAny(mode[len(mode)-1:], "1357") - } - if !passable { - problems = append(problems, fmt.Sprintf("%s's own secret %s is at %s; a tools bundle's account is read by the "+ - "node's tool runtime, so it lies in a directory resource of the module that others may pass through "+ - "(mode 0711 or 0755, `${dir:}/%s`), the file itself the runtime account's, 0600 (novox/hq ADR 0564 "+ - "Limits 4)", m.Module, name, own.Path, name)) + if !strings.HasPrefix(own.Path, "${dir:"+dir+"}/") || !passableDir(m, dir, 0) { + problems = append(problems, fmt.Sprintf("%s's own secret %s is at %s, which the node's tool runtime cannot "+ + "reach: a tools bundle's account lies in a directory resource the runtime's account may pass, every parent "+ + "included, such as {\"id\":\"tools-secret\",\"type\":\"directory\",\"path\":\"%s/%s-tools\",\"mode\":\"0711\"} "+ + "with the secret at ${dir:tools-secret}/%s; never under the module's own or mesh place, which are its "+ + "own (novox/hq ADR 0564 Limits 4)", m.Module, name, own.Path, meshStateRoot, m.Module, name)) } } return problems } +// meshStateRoot is the mesh's directory of every module's mesh place on a node of the default data root: root's, mode +// 0755, so the runtime's account passes it, while each module's own place beneath it is 0700. +const meshStateRoot = defaultDataRoot + "/" + placeMesh + +// passableDir says the runtime's account may pass the directory resource id and every parent the mesh makes for it: its +// own mode lets others pass (the last digit odd), and it is either stated directly under meshStateRoot or beneath a +// placed directory that is passable in turn. A pathless directory resolves under the module's own root or its mesh +// place, which are the module's own, so it never is. +func passableDir(m Manifest, id string, depth int) bool { + if depth > 4 { + return false + } + for _, r := range m.Resources { + if fmt.Sprint(r["type"]) != "directory" || fmt.Sprint(r["id"]) != id { + continue + } + mode, _ := r["mode"].(string) + if mode == "" || !strings.ContainsAny(mode[len(mode)-1:], "1357") { + return false + } + path, _ := r["path"].(string) + switch { + case strings.HasPrefix(path, "${dir:"): + parent, _, _ := strings.Cut(strings.TrimPrefix(path, "${dir:"), "}") + return passableDir(m, parent, depth+1) + case strings.HasPrefix(path, "/"): + return filepathDir(strings.TrimRight(path, "/")) == meshStateRoot + } + return false + } + return false +} + +func filepathDir(p string) string { + if i := strings.LastIndex(p, "/"); i > 0 { + return p[:i] + } + return "/" +} + // toolsBusProblems is what is wrong with a tools bundle's bus (ADR 0564 Limits 4): a bus on anything but a bundle // compiled to a binary the runtime loads, a removal outside the closed set, or a removal beside any other grant or by a // module that does not hold mesh-broker. Every other field is judged as a process's. diff --git a/go/catalogue/process_bus_test.go b/go/catalogue/process_bus_test.go index cbd9b37..7b3f8f3 100644 --- a/go/catalogue/process_bus_test.go +++ b/go/catalogue/process_bus_test.go @@ -51,14 +51,14 @@ func TestAProcessDeclaresItsBusAndIsAccepted(t *testing.T) { // 1952 S5 (novox/hq ADR 0564 Limits 4; the lead, 2026-10-29): a tools bundle declares its own bus account, read as a // process's, named after its binary and kept through resolution; a removal is from the closed set, alone, and the // bus module's (mesh-broker) alone. - toolsIn := func(claims, bus, mode string) string { + toolsIn := func(claims, bus, mode, where string) string { return `{"module":"nats","version":"1","claims":` + claims + `,"own-secrets":{"broker":"${dir:mesh-state}/broker",` + `"broker-nats-tools":"${dir:tools-secret}/broker-nats-tools"},"resources":[{"id":"mesh-state","type":"directory",` + `"mode":"0700","place":"mesh","unknown":"program"},{"id":"tools-secret","type":"directory","mode":"` + mode + - `","place":"mesh","unknown":"program"}],"build":{"artifacts":[{"name":"tools","kind":"bundle",` + + `","path":"` + where + `","unknown":"program"}],"build":{"artifacts":[{"name":"tools","kind":"bundle",` + `"language":"go","system":"arch","from":"cmd/nats-tools","binary":"nats-tools","loads":["nats-tools"],"bus":` + bus + `}]}}` } - tools := func(claims, bus string) string { return toolsIn(claims, bus, "0711") } + tools := func(claims, bus string) string { return toolsIn(claims, bus, "0711", "/var/lib/mesh/nats-tools") } broker := `[{"name":"mesh-broker","scope":"mesh"}]` for _, c := range []struct { name, manifest, refused string @@ -70,8 +70,11 @@ func TestAProcessDeclaresItsBusAndIsAccepted(t *testing.T) { {name: "a tools bundle consuming a queue", manifest: tools(broker, `{"queues":["x"]}`), refused: "holds no consumer"}, {name: "a process removing", manifest: mailSenderProcessManifest(t, `{"removes":["mesh-host_acts"]}`), refused: "only a tools bundle"}, // The runtime's account reads the account's secret: it lies where others may pass (the host agent, 2026-10-29). - {name: "a tools secret the runtime cannot reach", manifest: toolsIn(broker, `{"removes":["mesh-host_acts"]}`, "0700"), - refused: "others may pass through"}, + {name: "a tools secret the runtime cannot reach", manifest: toolsIn(broker, `{"removes":["mesh-host_acts"]}`, "0700", + "/var/lib/mesh/nats-tools"), refused: "cannot reach"}, + // Its parents too: under the module's mesh place, /var/lib/mesh/nats is 0700 (the lead, read on novox 2026-10-29). + {name: "a tools secret beneath the module's mesh place", manifest: toolsIn(broker, `{"removes":["mesh-host_acts"]}`, "0711", + "${dir:mesh-state}/tools"), refused: "cannot reach"}, } { m, err := ParseManifest([]byte(c.manifest)) if c.refused != "" {