A consumer is a module on a machine, not a machine
novox/hq 04-ISSUES/022. A credential was keyed by provision, consumer node and provider node, so "who is asking" was answered by naming a host. The node this mesh exists to take over runs eight modules against one database server. The symptom had two halves and only one was loud. The provider refused, naming the modules and explaining they would share one credential, which reads as a decision rather than a limit. The consumer did not refuse: it resolved cleanly, wrote one module's credential file and left the others absent — a service that starts and cannot authenticate, with nothing saying why. That is 021 again on a different axis. Three modules wanting one database produced one need, carrying whichever module mentioned it first, because the resolution walk is a work-list over names. The fan-out now happens in one place, after the walk. The record path already did this correctly and said why: a consumer here is a module on a machine. It is the same rule. Downstream: the secret's key gains the consuming module, the grant file is named after both halves, needs are matched by provision and module rather than provision alone, and the provisioners name the role and the access key after the module. The refusal in ContributionsTo is gone because there is nothing left to refuse. Worth stating plainly: without that refusal, gitea's login would have opened keycloak's database. From the provisioner's side it created exactly what it was asked to create. Existing secrets are discarded rather than backfilled. They cannot say which module they were for, and a secret is remade and delivered to both ends on the next push — so this costs one rotation and invents nothing. Also guards the role name against PostgreSQL's 63-byte truncation, which is a notice rather than an error and would reintroduce exactly this collision at a length nobody tests. Three faults injected — the fan-out removed, needs matched by name alone, the grant file named after the machine — each caught.
This commit is contained in:
@@ -91,14 +91,14 @@ func planFor(ctx context.Context, open *stores, nodeName string) (catalogue.Reso
|
||||
resolved.Needs[i].Sealed = sealed
|
||||
continue
|
||||
}
|
||||
secret, err := inv.SecretFor(ctx, n.Name, nodeName, n.From)
|
||||
secret, err := inv.SecretFor(ctx, n.Name, nodeName, n.For, n.From)
|
||||
if err != nil {
|
||||
// Said rather than skipped. A machine that resolves cleanly and receives no
|
||||
// credential is one that will fail to authenticate at some later, less obvious
|
||||
// moment.
|
||||
return catalogue.Resolution{}, nil, fmt.Errorf(
|
||||
"%s needs %s from %s and no credential could be made for it: %w",
|
||||
nodeName, n.Name, n.From, err)
|
||||
"%s on %s needs %s from %s and no credential could be made for it: %w",
|
||||
n.For, nodeName, n.Name, n.From, err)
|
||||
}
|
||||
resolved.Needs[i].Sealed = secret.ForConsumer
|
||||
}
|
||||
@@ -403,10 +403,17 @@ func grantsFor(ctx context.Context, open *stores, node string) ([]catalogue.Gran
|
||||
// run would have the provider create a user nothing uses.
|
||||
continue
|
||||
}
|
||||
from, values, err := plan.ContributionsTo(s.Name, settings)
|
||||
values, asks, err := plan.ContributionsFrom(s.Name, s.ConsumerModule, settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
from := s.ConsumerModule
|
||||
if !asks {
|
||||
// That module no longer wants this. Left empty, which is what the declaration reads
|
||||
// as "nobody asks for it any more" — and is how a login is withdrawn rather than kept
|
||||
// working for ever after its consumer went away.
|
||||
from = ""
|
||||
}
|
||||
out = append(out, catalogue.Grant{
|
||||
Provision: s.Name, Consumer: s.Consumer, At: onNetwork[s.Consumer],
|
||||
From: from, Values: values, Sealed: s.ForProvider})
|
||||
|
||||
@@ -81,19 +81,21 @@ func rotateCommand(ctx context.Context, args []string) error {
|
||||
|
||||
fmt.Printf("rotating %s for %d holder(s):\n", provision, len(holders))
|
||||
for _, h := range holders {
|
||||
fmt.Printf(" %s from %s\n", h.Consumer, h.Provider)
|
||||
// The module, because a machine may hold several credentials for one provision and
|
||||
// rotating "anchor's database password" now means rotating three of them.
|
||||
fmt.Printf(" %s on %s, from %s\n", h.ConsumerModule, h.Consumer, h.Provider)
|
||||
}
|
||||
|
||||
for _, h := range holders {
|
||||
if err := inv.RotateSecret(ctx, h.Provision, h.Consumer, h.Provider); err != nil {
|
||||
if err := inv.RotateSecret(ctx, h.Provision, h.Consumer, h.ConsumerModule, h.Provider); err != nil {
|
||||
// Partly rotated, and said so plainly. What is gone is remade on the next push, so
|
||||
// the remedy is to run this again rather than to repair anything — but a machine
|
||||
// whose secret was discarded and not resent is holding a credential the provider is
|
||||
// about to stop honouring, and that is worth knowing now.
|
||||
return fmt.Errorf(
|
||||
"rotating %s for %s from %s: %w\n\nSome credentials were discarded and not yet "+
|
||||
"sent. Run this again once the cause is fixed",
|
||||
h.Provision, h.Consumer, h.Provider, err)
|
||||
"rotating %s for %s on %s from %s: %w\n\nSome credentials were discarded and "+
|
||||
"not yet sent. Run this again once the cause is fixed",
|
||||
h.Provision, h.ConsumerModule, h.Consumer, h.Provider, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user