A requirement answered on this machine is still a requirement
novox/hq 04-ISSUES/021. Two modules where one provided what the other required, on one node, resolved cleanly with zero needs: no credential was made, the consumer's secret file was never written, and whatever read it would fail somewhere else entirely. Nothing was refused and nothing was reported. The world a node resolves against is every OTHER node, so a provider on the same machine never became a Needed, and the credential loop walks Needs. Every step reasonable, the sum a silent gap. It survived because everything proven until now was cross-machine — the interesting case for a mesh and the rare one in practice. The first module to want a database on its own machine was the first real one. The assumption underneath was that a local consumer needs no credential, which holds for a process reaching a unix socket where the system can vouch for the caller. It does not hold for containers, which is how nearly everything here runs: the consumer reaches the provider over TCP from its own container and the database asks for a password exactly as it would from another machine. **The machine stops being a trust boundary once both ends are containers.** A brokered provision answered here is now a need naming this node, and carries what the provider serves — which a local provider never contributes through the world. A name nothing grants is unchanged: a shell answered here is answered, and nothing more is owed. Both directions tested, both injections bite.
This commit is contained in:
@@ -231,7 +231,21 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, world
|
|||||||
}
|
}
|
||||||
// Already answered by something in the set. This is the case that makes assigning zsh do
|
// Already answered by something in the set. This is the case that makes assigning zsh do
|
||||||
// what a person meant by it.
|
// what a person meant by it.
|
||||||
|
//
|
||||||
|
// **Except when the thing answering it grants a credential** (novox/hq 04-ISSUES/021). A
|
||||||
|
// shell answered here needs nothing more; a database answered here still needs a
|
||||||
|
// password, because the consumer reaches it over TCP from its own container and the
|
||||||
|
// database asks exactly as it would from another machine. **The machine stops being a
|
||||||
|
// trust boundary the moment both ends are containers**, and treating it as one gave the
|
||||||
|
// commonest arrangement of all — a service and its database on one node — the weakest
|
||||||
|
// handling, silently.
|
||||||
if satisfied[want] && !isModule(catalogue, want) {
|
if satisfied[want] && !isModule(catalogue, want) {
|
||||||
|
if brokered[want] {
|
||||||
|
// Answered here, and still a need: the provider is this node.
|
||||||
|
needs = append(needs, Needed{
|
||||||
|
Name: want, From: node.Name, At: node.At,
|
||||||
|
Serves: servedHere(catalogue, chosen, want), For: because[want]})
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,6 +453,22 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, world
|
|||||||
return resolution, nil
|
return resolution, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// servedHere is what a provider in this node's own set says a consumer needs to know.
|
||||||
|
//
|
||||||
|
// The same facts a provider elsewhere would have contributed through the world, taken from the
|
||||||
|
// module directly because a provider on this machine never passes through it.
|
||||||
|
func servedHere(catalogue map[string]Manifest, chosen map[string]bool, want string) map[string]any {
|
||||||
|
for name, m := range catalogue {
|
||||||
|
if !chosen[name] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if serves, ok := m.Serves[want]; ok {
|
||||||
|
return serves
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// isModule reports whether a name is a module in its own right rather than only something
|
// isModule reports whether a name is a module in its own right rather than only something
|
||||||
// modules provide.
|
// modules provide.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -335,3 +335,59 @@ func mustDeclare(t *testing.T, r Resolution) []map[string]any {
|
|||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A requirement answered on the same machine is still a requirement (novox/hq 04-ISSUES/021).
|
||||||
|
//
|
||||||
|
// **The machine stops being a trust boundary once both ends are containers.** A consumer reaches
|
||||||
|
// its provider over TCP from its own container, and the database asks for a password exactly as it
|
||||||
|
// would from another machine. Before this, two such modules resolved cleanly with zero needs: no
|
||||||
|
// credential was made, the consumer's secret file was never written, and whatever read it failed
|
||||||
|
// somewhere else entirely.
|
||||||
|
//
|
||||||
|
// It went unnoticed because everything proven until then was cross-machine, which is the
|
||||||
|
// interesting case for a mesh and the rare one in practice.
|
||||||
|
func TestSomethingAnsweredOnThisMachineIsStillANeed(t *testing.T) {
|
||||||
|
provider := Manifest{
|
||||||
|
Module: "postgres", Version: "1",
|
||||||
|
Provides: FromAnywhere("postgres-database"),
|
||||||
|
Serves: map[string]map[string]any{"postgres-database": {"port": 5432}},
|
||||||
|
Grants: map[string]string{"postgres-database": "/var/lib/postgres/grants"},
|
||||||
|
}
|
||||||
|
consumer := Manifest{
|
||||||
|
Module: "keycloak", Version: "1",
|
||||||
|
Requires: []string{"postgres-database"},
|
||||||
|
Secrets: map[string]string{"postgres-database": "/var/lib/keycloak/database.env"},
|
||||||
|
}
|
||||||
|
got, err := Resolve(shelf(provider, consumer), []string{"postgres", "keycloak"},
|
||||||
|
Node{Name: "anchor", At: "10.0.0.1", Capabilities: map[string]bool{}}, World{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(got.Needs) != 1 {
|
||||||
|
t.Fatalf("%d need(s); a consumer of a brokered provision needs a credential wherever "+
|
||||||
|
"the provider is", len(got.Needs))
|
||||||
|
}
|
||||||
|
if got.Needs[0].From != "anchor" {
|
||||||
|
t.Errorf("the need names %q as the provider, and it is this machine", got.Needs[0].From)
|
||||||
|
}
|
||||||
|
// And it carries what the provider says a consumer must know, which a local provider never
|
||||||
|
// contributes through the world.
|
||||||
|
if got.Needs[0].Serves["port"] != 5432 {
|
||||||
|
t.Errorf("the need carries %v, and the provider serves port 5432", got.Needs[0].Serves)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A name nothing grants is unchanged: answered here means answered, and nothing more is owed.
|
||||||
|
func TestSomethingOrdinaryAnsweredHereNeedsNothing(t *testing.T) {
|
||||||
|
got, err := Resolve(shelf(
|
||||||
|
mod("zsh", []string{"shell"}, nil, nil),
|
||||||
|
mod("editor-user", nil, []string{"shell"}, nil),
|
||||||
|
), []string{"zsh", "editor-user"},
|
||||||
|
Node{Name: "anchor", At: "10.0.0.1", Capabilities: map[string]bool{}}, World{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(got.Needs) != 0 {
|
||||||
|
t.Fatalf("a shell answered on this machine produced %d need(s)", len(got.Needs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user