An operator delivers a pair credential, and the mesh never replaces it

secret accept grows --provider: the value is sealed to the consumer's node, the
provider's node and the operator's key, and the pair records origin 'accepted'.
An accepted pair is not remade when a key changes (the mesh does not hold the
value; the read is refused naming the remedy) and rotate refuses it (accepting a
new value is the rotation). The vault's third species has its entry
(novox/hq 04-ISSUES/070, ADR 0092).
This commit is contained in:
2026-09-21 17:50:41 +02:00
parent 537ad544d3
commit 396e05bb65
4 changed files with 188 additions and 4 deletions
+92 -3
View File
@@ -29,8 +29,17 @@ type Secret struct {
ForProvider string
ConsumerKey string
ProviderKey string
// Origin is `made` — the mesh generated it — or `accepted` — a person supplied it, for
// something outside the mesh, and the mesh cannot make another (novox/hq 04-ISSUES/070).
Origin string
}
// Where a pair credential came from.
const (
OriginMade = "made"
OriginAccepted = "accepted"
)
// SecretFor is the credential one module uses for one provision, making it the first time.
//
// **Made once and kept**, rather than regenerated whenever it is asked for. A secret that changed
@@ -64,15 +73,26 @@ func (i *Inventory) SecretFor(ctx context.Context, name, consumer, consumerModul
var held Secret
err = i.store.Pool().QueryRow(ctx,
`select for_consumer, for_provider, consumer_key, provider_key from secret
`select for_consumer, for_provider, consumer_key, provider_key, origin from secret
where name = $1 and consumer = $2 and consumer_module = $3 and provider = $4`,
name, consumerNode.ID, consumerModule, providerNode.ID).
Scan(&held.ForConsumer, &held.ForProvider, &held.ConsumerKey, &held.ProviderKey)
Scan(&held.ForConsumer, &held.ForProvider, &held.ConsumerKey, &held.ProviderKey, &held.Origin)
if err == nil && held.ConsumerKey == consumerKey && held.ProviderKey == providerKey {
held.Name, held.Consumer, held.Provider = name, consumer, provider
held.ConsumerModule = consumerModule
return held, nil
}
if err == nil && held.Origin == OriginAccepted {
// A person supplied this, and the mesh does not hold the value: it cannot seal it to the
// new key. Refused aloud rather than replaced by something the mesh made up, which would
// be delivered, reported as applied, and fail to authenticate somewhere else entirely
// (novox/hq 04-ISSUES/070).
return Secret{}, fmt.Errorf(
"%s's %q credential from %s was accepted from a person, and a sealing key at one end "+
"has changed since. The mesh cannot re-seal a value it does not hold: accept it "+
"again with `secret accept %s %s %s --provider %s`",
consumerModule, name, provider, consumer, consumerModule, name, provider)
}
// And to the operator, when the mesh has one (novox/hq ADR 0085, amended): the third copy that
// makes a vault-provided secret recoverable, and nothing the mesh can open.
@@ -102,7 +122,60 @@ func (i *Inventory) SecretFor(ctx context.Context, name, consumer, consumerModul
return Secret{Name: name, Consumer: consumer, ConsumerModule: consumerModule,
Provider: provider,
ForConsumer: made.ForConsumer, ForProvider: made.ForProvider,
ConsumerKey: made.ConsumerKey, ProviderKey: made.ProviderKey}, nil
ConsumerKey: made.ConsumerKey, ProviderKey: made.ProviderKey, Origin: OriginMade}, nil
}
// AcceptSecretForPair takes a value a person supplied into a pair credential — sealed to the
// consumer's node and to the provider's, and to the operator when the mesh has one — where the
// mesh would otherwise have made one (novox/hq 04-ISSUES/070, ADR 0092).
//
// This is the vault's third species: a credential for something outside the mesh, which only a
// person can supply. It is the counterpart to AcceptSecretForModule for a module's own secret;
// what differs is that both ends of the pair are sealed to, and that the record says `accepted`
// so a later read never replaces it with a minted one. The plaintext is discarded here.
func (i *Inventory) AcceptSecretForPair(ctx context.Context, name, consumer, consumerModule, provider, value string) error {
consumerKey, err := i.SealingKeyOf(ctx, consumer)
if err != nil {
return err
}
providerKey, err := i.SealingKeyOf(ctx, provider)
if err != nil {
return err
}
if consumerKey == "" || providerKey == "" {
return fmt.Errorf(
"both %s and %s need a sealing key before a credential can be sealed to them — a "+
"node joins to get one", consumer, provider)
}
consumerNode, err := i.NodeByName(ctx, consumer)
if err != nil {
return err
}
providerNode, err := i.NodeByName(ctx, provider)
if err != nil {
return err
}
sealed, err := secrets.Accept(value, consumerKey, providerKey)
if err != nil {
return err
}
forOperator, operatorKey, err := i.operatorSeal(ctx, value)
if err != nil {
return err
}
_, err = i.store.Pool().Exec(ctx,
`insert into secret (name, consumer, consumer_module, provider, for_consumer, for_provider,
consumer_key, provider_key, operator_sealed, operator_key, origin)
values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
on conflict (name, consumer, consumer_module, provider) do update set
for_consumer = excluded.for_consumer, for_provider = excluded.for_provider,
consumer_key = excluded.consumer_key, provider_key = excluded.provider_key,
created_at = now(), origin = excluded.origin,
operator_sealed = excluded.operator_sealed, operator_key = excluded.operator_key`,
name, consumerNode.ID, consumerModule, providerNode.ID,
sealed.ForConsumer, sealed.ForProvider, sealed.ConsumerKey, sealed.ProviderKey,
forOperator, operatorKey, OriginAccepted)
return err
}
// RotateSecret discards what was there, so the next declaration carries a new one.
@@ -113,6 +186,10 @@ func (i *Inventory) SecretFor(ctx context.Context, name, consumer, consumerModul
//
// The new secret then reaches both ends on the same push, together, which is what makes rotation
// a single event rather than a fanout with a window where half the mesh holds a dead credential.
//
// **An accepted credential is not rotated.** The mesh did not make it and cannot make its
// replacement; deleting it would have the next read mint one, which is exactly the wrong value
// delivered with the mesh insisting it was (novox/hq 04-ISSUES/070). Refused, and the remedy named.
func (i *Inventory) RotateSecret(ctx context.Context, name, consumer, consumerModule, provider string) error {
consumerNode, err := i.NodeByName(ctx, consumer)
if err != nil {
@@ -122,6 +199,18 @@ func (i *Inventory) RotateSecret(ctx context.Context, name, consumer, consumerMo
if err != nil {
return err
}
var origin string
err = i.store.Pool().QueryRow(ctx,
`select origin from secret where name = $1 and consumer = $2 and consumer_module = $3
and provider = $4`,
name, consumerNode.ID, consumerModule, providerNode.ID).Scan(&origin)
if err == nil && origin == OriginAccepted {
return fmt.Errorf(
"%s's %q credential from %s was accepted from a person, and the mesh cannot make "+
"its replacement. Accept the new value instead: `secret accept %s %s %s "+
"--provider %s --from <file>`",
consumerModule, name, provider, consumer, consumerModule, name, provider)
}
_, err = i.store.Pool().Exec(ctx,
`delete from secret where name = $1 and consumer = $2 and consumer_module = $3
and provider = $4`,