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
|
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 {
|
for _, g := range sorted {
|
||||||
if g.From == "" {
|
if g.From == "" {
|
||||||
// As above: nothing on that machine asks for this any more, so the provider is not
|
// 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)),
|
As: ConsumerIdentity(g.Consumer, IdentitySource(g.Slug, g.From)),
|
||||||
Secret: grantPath(directories[g.Provision], g.Consumer, 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 _, m := range modules {
|
||||||
for _, to := range sortedKeys(m.Contributes) {
|
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
|
// 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
|
// 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.
|
// 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)
|
"${secret:%s} could mean either — rename one of them", m.Module, to, to, to)
|
||||||
}
|
}
|
||||||
for i := range needs {
|
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
|
sealed[to] = needs[i].Sealed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ func TestAGrantedCredentialCanBeShapedIntoAnEnvFile(t *testing.T) {
|
|||||||
"mode": "0600",
|
"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{})
|
out, err := r.Declaration(Rendering{})
|
||||||
if err != nil {
|
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.
|
// A name nothing declared is a typo, and it is refused here rather than on the machine.
|
||||||
func TestAFileNamingASecretTheModuleDoesNotHaveIsRefused(t *testing.T) {
|
func TestAFileNamingASecretTheModuleDoesNotHaveIsRefused(t *testing.T) {
|
||||||
r := Resolution{Node: "anchor", Modules: []Manifest{{
|
r := Resolution{Node: "anchor", Modules: []Manifest{{
|
||||||
|
|||||||
Reference in New Issue
Block a user