fix: a ${secret:} placeholder must fill from the file-owner's credential (provider-seal-key gate) #10
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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{{
|
||||
|
||||
Reference in New Issue
Block a user