Merge pull request 'fix: a ${secret:} placeholder must fill from the file-owner's credential (provider-seal-key gate)' (#10) from fix/secret-per-consumer into main

This commit was merged in pull request #10.
This commit is contained in:
2026-09-06 13:47:22 +02:00
3 changed files with 68 additions and 2 deletions
+16
View File
@@ -496,6 +496,11 @@ func (r Resolution) contributions(settings SettingsBy, grants []Grant,
}
return sorted[i].From < sorted[j].From
})
// A consumer already carried by the grants loop, keyed (provision, module). When provider and
// consumer are co-located, `grantsFor` enumerates the same-node consumer too, so without this the
// module would be emitted a second time by the m.Contributes loop below — once full (with the
// grant's secret/as) and once partial — which is the duplicate seen in a co-located mesh.json.
granted := map[string]map[string]bool{}
for _, g := range sorted {
if g.From == "" {
// As above: nothing on that machine asks for this any more, so the provider is not
@@ -507,9 +512,20 @@ func (r Resolution) contributions(settings SettingsBy, grants []Grant,
As: ConsumerIdentity(g.Consumer, IdentitySource(g.Slug, g.From)),
Secret: grantPath(directories[g.Provision], g.Consumer, g.From),
})
if granted[g.Provision] == nil {
granted[g.Provision] = map[string]bool{}
}
granted[g.Provision][g.From] = true
}
for _, m := range modules {
for _, to := range sortedKeys(m.Contributes) {
// The grants loop already emitted this module's contribution to this provision, with its
// minted secret — emitting the partial copy again would duplicate it. A contribution with
// no grant (a reverse-proxy route names a host, not a credential) is not in `granted`, so
// it still reaches the provider from here.
if granted[to][m.Module] {
continue
}
// Settings reach a contribution the same way they reach a file. A route's hostname is
// exactly the kind of thing that differs between one mesh and the next, and a module
// that could not have it set would have to be edited to be reused.
+5 -1
View File
@@ -71,7 +71,11 @@ func sealedFor(m Manifest, needs []Needed, with Rendering) (map[string]string, e
"${secret:%s} could mean either — rename one of them", m.Module, to, to, to)
}
for i := range needs {
if needs[i].Name == to && needs[i].Sealed != "" {
// `For == m.Module`, not name alone: on a node with two modules requiring the same
// provision, both appear in `needs`, and matching by name would fill ${secret:X} with
// whichever came last — the other module's credential (novox/hq 04-ISSUES/022). The
// `secrets:`-map path already guards this way; the ${secret:…} placeholder path did not.
if needs[i].Name == to && needs[i].For == m.Module && needs[i].Sealed != "" {
sealed[to] = needs[i].Sealed
}
}
+47 -1
View File
@@ -67,7 +67,7 @@ func TestAGrantedCredentialCanBeShapedIntoAnEnvFile(t *testing.T) {
"mode": "0600",
}},
}},
Needs: []Needed{{Name: "postgres-database", From: "anchor", Sealed: "sealed-db"}},
Needs: []Needed{{Name: "postgres-database", For: "keycloak", From: "anchor", Sealed: "sealed-db"}},
}
out, err := r.Declaration(Rendering{})
if err != nil {
@@ -80,6 +80,52 @@ func TestAGrantedCredentialCanBeShapedIntoAnEnvFile(t *testing.T) {
}
}
// Two modules on one node requiring the same provision each get THEIR OWN credential in a
// ${secret:…} file — not whichever need happened to come last. This is the placeholder-path twin of
// the guard novox/hq 04-ISSUES/022 put on the secrets:-map path; without it, a node with baserow and
// letta both consuming postgres gave baserow letta's password and authentication failed.
func TestTwoConsumersOfOneProvisionEachGetTheirOwnInAPlaceholderFile(t *testing.T) {
r := Resolution{
Node: "anchor",
Modules: []Manifest{
{
Module: "keycloak",
Requires: []string{"postgres-database"},
Secrets: map[string]string{"postgres-database": "/var/lib/keycloak/db.secret"},
Resources: []map[string]any{{
"id": "dbenv", "type": "file", "path": "/var/lib/keycloak/database.env",
"content": "PW=${secret:postgres-database}\n", "mode": "0600",
}},
},
{
Module: "grafana",
Requires: []string{"postgres-database"},
Secrets: map[string]string{"postgres-database": "/var/lib/grafana/db.secret"},
Resources: []map[string]any{{
"id": "dbenv", "type": "file", "path": "/var/lib/grafana/database.env",
"content": "PW=${secret:postgres-database}\n", "mode": "0600",
}},
},
},
Needs: []Needed{
{Name: "postgres-database", For: "keycloak", From: "anchor", Sealed: "sealed-kc"},
{Name: "postgres-database", For: "grafana", From: "anchor", Sealed: "sealed-gf"},
},
}
out, err := r.Declaration(Rendering{})
if err != nil {
t.Fatal(err)
}
kc, _ := fileNamed(out, "keycloak.dbenv")["secrets"].(map[string]any)
if kc["postgres-database"] != "sealed-kc" {
t.Errorf("keycloak got the wrong credential (someone else's password): %v", kc)
}
gf, _ := fileNamed(out, "grafana.dbenv")["secrets"].(map[string]any)
if gf["postgres-database"] != "sealed-gf" {
t.Errorf("grafana got the wrong credential (someone else's password): %v", gf)
}
}
// A name nothing declared is a typo, and it is refused here rather than on the machine.
func TestAFileNamingASecretTheModuleDoesNotHaveIsRefused(t *testing.T) {
r := Resolution{Node: "anchor", Modules: []Manifest{{