A grant secret belongs to whoever provisions, and the sweep skips what it will not address
Issue 225. The mesh seals one credential per consumer beside the provider's contributions file, and wrote it root-owned. That was right while a module's own code ran in a container as root; ADR 0198 moved that code under the node's runtime, as the node's account, and the secret stayed root's. On the control machine two consumers went unprovisioned for three hours and the only sign was a line reading 'secret not readable yet', 4330 times. The same sentence is already written for a module's own secrets a few hundred lines above — 'a root-owned 0600 file is one that process cannot read'. This is that rule reaching the other kind of secret the mesh writes for a module. Issue 226. The sweep met a reference recorded with the store's old address, read 'I will not address this' as 'the store refuses everything', and collected none of the 1681 it had found. Two changes: references from build records are read through Recorded, where the provenance is known — not in LetGo, which cannot tell one registry host from another and must stay strict — and a reference the sweep will not address is now ErrNotOurs, skipped, never a reason to stop. Only the store refusing ends a sweep. make check: the two failures both fail on main as well — the resolver test (hq 202/203) and the service-manager test, which reads this machine's own shell environment.
This commit is contained in:
@@ -610,14 +610,14 @@ func (r Resolution) compose(with Rendering, owner map[string]string,
|
||||
// and nothing would say so.
|
||||
continue
|
||||
}
|
||||
first = append(first, map[string]any{
|
||||
first = append(first, ownedBy(r.provisionsAs(m), map[string]any{
|
||||
// One file per holder — the consumer's module with its local name after it
|
||||
// where it keeps several (ADR 0094); the lab found two files with one id.
|
||||
"id": GrantID(to, g.Consumer+"."+holderAs(g.From, g.Local)),
|
||||
"type": "file",
|
||||
"path": grantPath(m.Grants[to], g.Consumer, holderAs(g.From, g.Local)),
|
||||
"sealed": g.Sealed,
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
for _, to := range sortedKeys(m.Binds) {
|
||||
@@ -1240,6 +1240,28 @@ type Contribution struct {
|
||||
Derived map[string]any `json:"derived,omitempty"`
|
||||
}
|
||||
|
||||
// provisionsAs is the account that reads what the mesh writes for this provider: the one secret
|
||||
// per consumer it must open to set that consumer's password (novox/hq issue 225).
|
||||
//
|
||||
// **A root-owned 0600 file is one that process cannot read**, which is the same sentence already
|
||||
// written above for a module's own secrets — and the grant secret is the other kind of secret
|
||||
// the mesh writes for a module, so it is the same rule.
|
||||
//
|
||||
// Which account depends on where the module's code runs. A module whose code is a bundle is run
|
||||
// by the node's tool runtime, as the node's account ([ADR 0198](0198)); one still in a container
|
||||
// is whatever it declares as its secrets owner. Nothing names these paths, so the rule that
|
||||
// claims a bundle's other files by the words that name them (givenTo) cannot reach them: the
|
||||
// harness composes a grant secret's path from the contributions file, not from a word.
|
||||
//
|
||||
// Empty is root, which is what it was and what a module with no bundle and no declared owner
|
||||
// still wants.
|
||||
func (r Resolution) provisionsAs(m Manifest) string {
|
||||
if len(m.Bundles) > 0 && r.Account != "" {
|
||||
return r.Account
|
||||
}
|
||||
return m.SecretsOwner
|
||||
}
|
||||
|
||||
// grantPath is where one consumer's sealed credential lands on the providing machine.
|
||||
//
|
||||
// Suffixed, so the directory can also hold whatever the module writing it keeps there and so a
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A grant secret is read by whatever provisions, and that stopped being root (novox/hq issue 225).
|
||||
//
|
||||
// The mesh seals one credential per consumer beside the provider's contributions file. The
|
||||
// provider's harness reads both: the file to learn who asked, the secret to set their password.
|
||||
// While a module's own code ran in a container as root, a root-owned 0600 file was readable by
|
||||
// the thing that needed it. ADR 0198 moved that code under the node's runtime, which runs as the
|
||||
// operator's account — and the secret stayed root's.
|
||||
//
|
||||
// **The cost was silence.** The harness says `secret not readable yet`, which is true and
|
||||
// ordinary on the first pass, so four thousand refusals in three hours read as patience. No user
|
||||
// was ever created, and two consumers crash-looped against a database that had never heard of
|
||||
// them.
|
||||
//
|
||||
// The same reasoning is already written for a module's *own* secrets, three hundred lines above:
|
||||
// "a root-owned 0600 file is one that process cannot read". This is that rule reaching the other
|
||||
// kind of secret the mesh writes for a module.
|
||||
|
||||
// aProviderWithABundle is a provider whose code is a bundle the node's runtime runs — the shape
|
||||
// every TypeScript provisioner has since ADR 0198.
|
||||
func aProviderWithABundle() Manifest {
|
||||
return Manifest{
|
||||
Module: "mongodb", Version: "1",
|
||||
Provides: FromAnywhere("mongodb-database"),
|
||||
Receives: map[string]string{"mongodb-database": "/var/lib/mongodb/grants/mesh.json"},
|
||||
Grants: map[string]string{"mongodb-database": "/var/lib/mongodb/grants"},
|
||||
Bundles: []Bundle{{Name: "code", Language: "typescript"}},
|
||||
Resources: []map[string]any{{
|
||||
"id": "server", "type": "container", "name": "mongodb-server",
|
||||
"image": "mongo@sha256:" + strings.Repeat("a", 64),
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
func TestAGrantSecretIsOwnedByTheAccountThatProvisions(t *testing.T) {
|
||||
r, err := Resolve(shelf(aProviderWithABundle()), []string{"mongodb"}, reachable(), World{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.Account = "operator"
|
||||
out, err := r.Declaration(Rendering{Grants: []Grant{{
|
||||
Provision: "mongodb-database", Consumer: "workstation", From: "photos", Slug: "photos",
|
||||
Values: map[string]any{}, Sealed: "c2VhbGVk",
|
||||
}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var secret map[string]any
|
||||
for _, res := range out {
|
||||
if res["type"] == "file" && strings.HasSuffix(fmtPath(res), ".secret") {
|
||||
secret = res
|
||||
}
|
||||
}
|
||||
if secret == nil {
|
||||
t.Fatalf("no grant secret was composed at all: %v", out)
|
||||
}
|
||||
if got := secret["owner"]; got != "operator" {
|
||||
t.Fatalf("the grant secret at %v belongs to %v; the provisioner runs as %q and a "+
|
||||
"root-owned 0600 file is one it cannot read — which is silent, because the harness "+
|
||||
"calls it \"not readable yet\"", fmtPath(secret), got, "operator")
|
||||
}
|
||||
}
|
||||
|
||||
// And a provider whose code still runs in a container keeps the owner it declares, so this
|
||||
// changes nothing for the modules the runtime has not taken.
|
||||
func TestAContainerProvidersGrantSecretKeepsItsDeclaredOwner(t *testing.T) {
|
||||
m := aProviderWithABundle()
|
||||
m.Bundles = nil
|
||||
m.SecretsOwner = "65534:65534"
|
||||
r, err := Resolve(shelf(m), []string{"mongodb"}, reachable(), World{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.Account = "operator"
|
||||
out, err := r.Declaration(Rendering{Grants: []Grant{{
|
||||
Provision: "mongodb-database", Consumer: "workstation", From: "photos", Slug: "photos",
|
||||
Values: map[string]any{}, Sealed: "c2VhbGVk",
|
||||
}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, res := range out {
|
||||
if res["type"] == "file" && strings.HasSuffix(fmtPath(res), ".secret") {
|
||||
if got := res["owner"]; got != "65534:65534" {
|
||||
t.Fatalf("a container provider's grant secret belongs to %v, not what it declares", got)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatal("no grant secret was composed")
|
||||
}
|
||||
|
||||
func fmtPath(r map[string]any) string {
|
||||
p, _ := r["path"].(string)
|
||||
return p
|
||||
}
|
||||
Reference in New Issue
Block a user