An offer may say `"credential": {"own": "<secret>"}`: the provider's own secret is the credential
every consumer of that provision receives, in the shape of a pair credential. The vault keeps one
value, sealed to the provider, to every consumer that holds the provision and to the operator, all
under one generation stamp; a consumer binding later, or `secret rotate` on the provider's secret,
makes a fresh value and seals it to every holder in one act, and the rotate command sends every
holding machine together. An accepted value is sealed to the consumers of the moment and never
remade: a consumer binding after it is refused with the way out (ADR 0113). The named own secret
must say how it is taken (issue 180), so the provider's start applies the file.
A need carries the shared secret's name from either side of the machine boundary; the plan mints a
consumer's copy from the provider's value. Registered manifests keep their bytes.
823 lines
33 KiB
Go
823 lines
33 KiB
Go
package inventory
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
"github.com/novox/mesh-controller/internal/secrets"
|
|
)
|
|
|
|
// Where sealed secrets live.
|
|
//
|
|
// The table holds nothing usable — see the migration and internal/secrets for why that is the
|
|
// design rather than an inconvenience.
|
|
|
|
// Secret is one provision's credential, sealed to each end.
|
|
type Secret struct {
|
|
Name string
|
|
Consumer string
|
|
// ConsumerModule is which module on that machine it is for.
|
|
//
|
|
// **Part of the key, not a label** (novox/hq 04-ISSUES/022). Two modules on one node wanting
|
|
// the same provision are two consumers, and were one credential until this.
|
|
ConsumerModule string
|
|
// Local is the name the credential goes by inside the consumer where it keeps several for one
|
|
// provision (novox/hq ADR 0094); empty for the ordinary one. Part of the key.
|
|
Local string
|
|
Provider string
|
|
ForConsumer string
|
|
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
|
|
// on every declaration would restart both ends on every push and would mean the password a
|
|
// provider was told to create never matches the one a consumer was given — which is a mesh that
|
|
// reports success and cannot connect.
|
|
//
|
|
// **Remade when either end's sealing key changes.** A node that rejoined generated a new key and
|
|
// can no longer open what was sealed to the old one, so keeping the blob would deliver something
|
|
// unreadable for ever. The new secret reaches both ends in the same push, which is the only
|
|
// moment they can be changed together.
|
|
func (i *Inventory) SecretFor(ctx context.Context, name, consumer, consumerModule, provider, local string) (
|
|
Secret, error) {
|
|
consumerKey, err := i.SealingKeyOf(ctx, consumer)
|
|
if err != nil {
|
|
return Secret{}, err
|
|
}
|
|
providerKey, err := i.SealingKeyOf(ctx, provider)
|
|
if err != nil {
|
|
return Secret{}, err
|
|
}
|
|
|
|
consumerNode, err := i.NodeByName(ctx, consumer)
|
|
if err != nil {
|
|
return Secret{}, err
|
|
}
|
|
providerNode, err := i.NodeByName(ctx, provider)
|
|
if err != nil {
|
|
return Secret{}, err
|
|
}
|
|
|
|
var held Secret
|
|
err = i.store.Pool().QueryRow(ctx,
|
|
`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 and local = $5`,
|
|
name, consumerNode.ID, consumerModule, providerNode.ID, local).
|
|
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, held.Local = consumerModule, local
|
|
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%s`",
|
|
consumerModule, name, provider, consumer, consumerModule, name, provider, localFlag(local))
|
|
}
|
|
|
|
// 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.
|
|
operator, err := i.OperatorKey(ctx)
|
|
if err != nil {
|
|
return Secret{}, err
|
|
}
|
|
made, blob, err := secrets.MakeWithOperator(consumerKey, providerKey, operator)
|
|
if err != nil {
|
|
return Secret{}, err
|
|
}
|
|
forOperator, operatorKey := operatorColumns(operator, blob)
|
|
_, 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, local)
|
|
values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
|
on conflict (name, local, 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(),
|
|
operator_sealed = excluded.operator_sealed, operator_key = excluded.operator_key`,
|
|
name, consumerNode.ID, consumerModule, providerNode.ID,
|
|
made.ForConsumer, made.ForProvider, made.ConsumerKey, made.ProviderKey, forOperator, operatorKey, local)
|
|
if err != nil {
|
|
return Secret{}, err
|
|
}
|
|
return Secret{Name: name, Consumer: consumer, ConsumerModule: consumerModule, Local: local,
|
|
Provider: provider,
|
|
ForConsumer: made.ForConsumer, ForProvider: made.ForProvider,
|
|
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, local, value string) error {
|
|
// Refused for a requirement the module does not have, or a local it does not keep under it
|
|
// (novox/hq 04-ISSUES/078): the credential would sit in the pair unread.
|
|
m, err := i.declared(ctx, consumerModule)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !slices.Contains(m.Requires, name) {
|
|
return fmt.Errorf("%s does not require %q; it requires: %s", consumerModule, name, orNone(m.Requires))
|
|
}
|
|
if locals := m.SecretsMany[name]; len(locals) > 0 {
|
|
if local == "" {
|
|
return fmt.Errorf("%s keeps several secrets for %q; name one with --local: %s",
|
|
consumerModule, name, orNone(sortedNames(locals)))
|
|
}
|
|
if _, kept := locals[local]; !kept {
|
|
return fmt.Errorf("%s does not keep %q for %q; it keeps: %s",
|
|
consumerModule, local, name, orNone(sortedNames(locals)))
|
|
}
|
|
} else if m.Secrets[name] == "" {
|
|
return fmt.Errorf("%s requires %q but keeps no secret for it, so a delivered value would sit unread; "+
|
|
"it keeps secrets for: %s", consumerModule, name, orNone(sortedNames(m.Secrets)))
|
|
} else if local != "" {
|
|
return fmt.Errorf("%s keeps one secret for %q, not several; drop --local", consumerModule, name)
|
|
}
|
|
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, local)
|
|
values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
|
on conflict (name, local, 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, local)
|
|
return err
|
|
}
|
|
|
|
// RotateSecret discards what was there, so the next declaration carries a new one.
|
|
//
|
|
// Only a delete. Nothing reads the old value first, because nothing can — and making the
|
|
// replacement here rather than on the next read would be a second path to the same act, which is
|
|
// how two ends come to hold different passwords.
|
|
//
|
|
// 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, local string) error {
|
|
consumerNode, err := i.NodeByName(ctx, consumer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
providerNode, err := i.NodeByName(ctx, provider)
|
|
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 and local = $5`,
|
|
name, consumerNode.ID, consumerModule, providerNode.ID, local).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%s --from <file>`",
|
|
consumerModule, name, provider, consumer, consumerModule, name, provider, localFlag(local))
|
|
}
|
|
_, err = i.store.Pool().Exec(ctx,
|
|
`delete from secret where name = $1 and consumer = $2 and consumer_module = $3
|
|
and provider = $4 and local = $5`,
|
|
name, consumerNode.ID, consumerModule, providerNode.ID, local)
|
|
return err
|
|
}
|
|
|
|
// SecretsFrom is every credential a provider node was issued, so it can be told what to create.
|
|
func (i *Inventory) SecretsFrom(ctx context.Context, provider string) ([]Secret, error) {
|
|
providerNode, err := i.NodeByName(ctx, provider)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rows, err := i.store.Pool().Query(ctx,
|
|
`select s.name, c.name, s.consumer_module, s.local, s.for_provider from secret s
|
|
join node c on c.id = s.consumer
|
|
where s.provider = $1 order by s.name, c.name, s.consumer_module, s.local`, providerNode.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var out []Secret
|
|
for rows.Next() {
|
|
s := Secret{Provider: provider}
|
|
if err := rows.Scan(&s.Name, &s.Consumer, &s.ConsumerModule, &s.Local, &s.ForProvider); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, s)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// SecretForModule is a secret a module needs in order to be itself, on one machine.
|
|
//
|
|
// Not the credential a consumer is given: a superuser password is not *for* anybody. Made once
|
|
// and kept, because regenerating it on every declaration would change the password a running
|
|
// database has already been started with — and remade when the node's sealing key changes, for
|
|
// the same reason as everything else sealed here.
|
|
// ModuleSecretIfIssued is what a module already holds on a node, and nothing if it holds nothing.
|
|
//
|
|
// **The read half of SecretForModule**, which mints one when there is none — an insert, and a row
|
|
// lock, on a path that also serves questions. Composing a declaration to answer *is this machine
|
|
// running what I would send it* went through the minting version for every module on every node,
|
|
// so asking wrote to the database and blocked against the machine it was asking about.
|
|
//
|
|
// A module with no secret yet has never been sent one, which is the same answer the caller wanted
|
|
// anyway: this machine is not running what the mesh would send it.
|
|
func (i *Inventory) ModuleSecretIfIssued(
|
|
ctx context.Context, node, module, name string,
|
|
) (string, bool, error) {
|
|
key, err := i.SealingKeyOf(ctx, node)
|
|
if err != nil || key == "" {
|
|
return "", false, err
|
|
}
|
|
record, err := i.NodeByName(ctx, node)
|
|
if err != nil {
|
|
return "", false, err
|
|
}
|
|
var sealed, against, origin string
|
|
err = i.store.Pool().QueryRow(ctx,
|
|
`select sealed, node_key, origin from module_secret
|
|
where node = $1 and module = $2 and name = $3`,
|
|
record.ID, module, name).Scan(&sealed, &against, &origin)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return "", false, nil
|
|
}
|
|
if err != nil {
|
|
return "", false, err
|
|
}
|
|
// Sealed to a key the node no longer has is not something it holds. Reported as absent rather
|
|
// than as an error: this is the read, and refusing here would make a question fail for a
|
|
// condition its writing counterpart is the right place to explain.
|
|
if against != key {
|
|
return "", false, nil
|
|
}
|
|
return sealed, true, nil
|
|
}
|
|
|
|
func (i *Inventory) SecretForModule(ctx context.Context, node, module, name string) (string, error) {
|
|
key, err := i.SealingKeyOf(ctx, node)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if key == "" {
|
|
return "", fmt.Errorf(
|
|
"%s needs a secret and %s has no sealing key, so nothing can be sealed to it",
|
|
module, node)
|
|
}
|
|
record, err := i.NodeByName(ctx, node)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var sealed, against, origin string
|
|
err = i.store.Pool().QueryRow(ctx,
|
|
`select sealed, node_key, origin from module_secret
|
|
where node = $1 and module = $2 and name = $3`,
|
|
record.ID, module, name).Scan(&sealed, &against, &origin)
|
|
if err == nil && against == key {
|
|
return sealed, nil
|
|
}
|
|
if err == nil && origin == "accepted" {
|
|
// Sealed to a key this node no longer has, and not the mesh's to invent again. Making one
|
|
// would put 32 random bytes where a working credential was: the machine would apply it,
|
|
// report success, and whatever reads it would fail to authenticate somewhere else
|
|
// entirely — with the mesh insisting the secret was delivered, which it was.
|
|
return "", fmt.Errorf(
|
|
"%s on %s holds %q, which was given to the mesh rather than made by it, and %s has "+
|
|
"since generated a new sealing key. The mesh cannot make another; issue it again",
|
|
module, node, name, node)
|
|
}
|
|
|
|
operator, err := i.OperatorKey(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
// Sealed once to the machine — Make seals to two ends because a provision has two; here both
|
|
// are the same machine, and only one copy is kept — and once more to the operator when the
|
|
// mesh has one (novox/hq ADR 0085, amended), which is the copy a person can recover from.
|
|
made, blob, err := secrets.MakeWithOperator(key, key, operator)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
forOperator, operatorKey := operatorColumns(operator, blob)
|
|
if _, err := i.store.Pool().Exec(ctx,
|
|
`insert into module_secret (node, module, name, sealed, node_key, origin, operator_sealed, operator_key)
|
|
values ($1, $2, $3, $4, $5, 'made', $6, $7)
|
|
on conflict (node, module, name) do update set
|
|
sealed = excluded.sealed, node_key = excluded.node_key,
|
|
origin = excluded.origin, made_at = now(),
|
|
operator_sealed = excluded.operator_sealed, operator_key = excluded.operator_key`,
|
|
record.ID, module, name, made.ForConsumer, key, forOperator, operatorKey); err != nil {
|
|
return "", err
|
|
}
|
|
return made.ForConsumer, nil
|
|
}
|
|
|
|
// AcceptSecretForModule keeps a value somebody supplied as a module's own secret.
|
|
//
|
|
// The counterpart to SecretForModule, which generates one. Some of what a module needs the mesh
|
|
// cannot invent: a broker account exists because the broker was told about it, and the password is
|
|
// whatever was agreed with the broker at that moment. The mesh's job is to carry it to the machine
|
|
// that will use it without being able to read it afterwards.
|
|
//
|
|
// Sealed on the way in and the plaintext discarded, exactly as a generated one is — so the only
|
|
// difference between the two is where the value came from.
|
|
func (i *Inventory) AcceptSecretForModule(ctx context.Context, node, module, name, value string) error {
|
|
// Refused for a name the module does not declare. A value stored under a name nothing reads
|
|
// is a delivery that changed nothing and reported success — the shape of failure the mesh
|
|
// is built to refuse (novox/hq 04-ISSUES/078).
|
|
m, err := i.declared(ctx, module)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, own := m.OwnSecrets[name]; !own {
|
|
return fmt.Errorf("%s does not declare %q as an own secret; %s", module, name, declaresOwn(m))
|
|
}
|
|
key, err := i.SealingKeyOf(ctx, node)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if key == "" {
|
|
return fmt.Errorf(
|
|
"%s has no sealing key, so nothing can be sealed to it — it joins again to get one",
|
|
node)
|
|
}
|
|
record, err := i.NodeByName(ctx, node)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
sealed, err := secrets.Accept(value, key, key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// And to the operator, when the mesh has one: a value a person supplied is the one a person
|
|
// most needs to get back, since the mesh cannot make another (novox/hq ADR 0085, amended).
|
|
forOperator, operatorKey, err := i.operatorSeal(ctx, value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = i.store.Pool().Exec(ctx,
|
|
`insert into module_secret (node, module, name, sealed, node_key, origin, operator_sealed, operator_key)
|
|
values ($1, $2, $3, $4, $5, 'accepted', $6, $7)
|
|
on conflict (node, module, name) do update set
|
|
sealed = excluded.sealed, node_key = excluded.node_key,
|
|
origin = excluded.origin, made_at = now(),
|
|
operator_sealed = excluded.operator_sealed, operator_key = excluded.operator_key`,
|
|
record.ID, module, name, sealed.ForConsumer, key, forOperator, operatorKey)
|
|
return err
|
|
}
|
|
|
|
// Holder is one end-to-end credential: who gets it and who must create it.
|
|
type Holder struct {
|
|
Provision string
|
|
Consumer string
|
|
// ConsumerModule is which module on that machine holds it. Part of what identifies a
|
|
// credential (novox/hq 04-ISSUES/022), so rotating one consumer's does not touch another's.
|
|
ConsumerModule string
|
|
// Local is the credential's name inside the consumer where it holds several (ADR 0094).
|
|
Local string
|
|
Provider string
|
|
}
|
|
|
|
// HoldersOf is every pair sharing a credential for one provision.
|
|
//
|
|
// **The question rotation has to ask, and the one HAL could not.** There, a provision had a single
|
|
// shared credential and rotating it updated the provider's row; nothing enumerated who else held
|
|
// the old one, so three nodes carried dead credentials for two days and the mesh reported success
|
|
// (novox/hq ADR 0001). Here each pair has its own credential, and this is the list that makes
|
|
// "every consumer" a set the mesh can name rather than a hope.
|
|
//
|
|
// Empty consumer means all of them.
|
|
func (i *Inventory) HoldersOf(ctx context.Context, provision, consumer string) ([]Holder, error) {
|
|
rows, err := i.store.Pool().Query(ctx,
|
|
`select s.name, c.name, s.consumer_module, s.local, p.name from secret s
|
|
join node c on c.id = s.consumer
|
|
join node p on p.id = s.provider
|
|
where s.name = $1 and ($2 = '' or c.name = $2)
|
|
order by c.name, s.consumer_module, s.local, p.name`, provision, consumer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var out []Holder
|
|
for rows.Next() {
|
|
var h Holder
|
|
if err := rows.Scan(&h.Provision, &h.Consumer, &h.ConsumerModule, &h.Local, &h.Provider); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, h)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// localFlag is the `--local` a remedy has to name where a credential has a local name.
|
|
func localFlag(local string) string {
|
|
if local == "" {
|
|
return ""
|
|
}
|
|
return " --local " + local
|
|
}
|
|
|
|
// declared is the manifest the mesh holds for a module — what a delivered value is checked
|
|
// against, so a delivery for a name the module does not have is refused rather than stored.
|
|
func (i *Inventory) declared(ctx context.Context, module string) (catalogue.Manifest, error) {
|
|
known, err := i.Catalogue(ctx)
|
|
if err != nil {
|
|
return catalogue.Manifest{}, err
|
|
}
|
|
m, ok := known[module]
|
|
if !ok {
|
|
return catalogue.Manifest{}, fmt.Errorf("%s is not a module the mesh knows; `module add` it first", module)
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func declaresOwn(m catalogue.Manifest) string {
|
|
if len(m.OwnSecrets) == 0 {
|
|
return "it declares no own secrets"
|
|
}
|
|
return "it declares: " + strings.Join(sortedNames(m.OwnSecrets.Paths()), ", ")
|
|
}
|
|
|
|
func sortedNames(of map[string]string) []string {
|
|
names := make([]string, 0, len(of))
|
|
for name := range of {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
return names
|
|
}
|
|
|
|
func orNone(names []string) string {
|
|
if len(names) == 0 {
|
|
return "none"
|
|
}
|
|
return strings.Join(names, ", ")
|
|
}
|
|
|
|
// ErrNotRotatable says why the mesh will not rotate a module's own secret; the words are the caller's
|
|
// to print, and the remedy is in them.
|
|
type ErrNotRotatable struct{ Why string }
|
|
|
|
func (e ErrNotRotatable) Error() string { return e.Why }
|
|
|
|
// RotateModuleSecret makes a module's own secret anew, the way the first mint did (novox/hq
|
|
// ADR 0114, issue 180). The caller sends the node, so the module is started again on the new value.
|
|
//
|
|
// **Only a secret the module reads when it starts.** A secret the module's code applies to a
|
|
// backend that takes it once would, rotated this way, leave the backend on the old value and the
|
|
// module reading the new one — the fault issue 179 was. That form is staged, which the mesh does
|
|
// not build yet, and is refused by name. A secret whose manifest says neither is refused with the
|
|
// word to write; a secret given to the mesh rather than made by it is refused as 0113 says: the
|
|
// mesh will not replace what it cannot read.
|
|
func (i *Inventory) RotateModuleSecret(ctx context.Context, node, module, name string) error {
|
|
m, err := i.declared(ctx, module)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
own, declared := m.OwnSecrets[name]
|
|
if !declared {
|
|
return fmt.Errorf("%s does not declare %q as an own secret; %s", module, name, declaresOwn(m))
|
|
}
|
|
switch own.Taken {
|
|
case catalogue.TakenAtStart:
|
|
case catalogue.TakenApplied:
|
|
return ErrNotRotatable{Why: fmt.Sprintf(
|
|
"%s applies %q to a backend that takes it once, so a rotation must be staged beside the "+
|
|
"current value until the module confirms it — the mesh does not do that yet (ADR 0114). "+
|
|
"Changing it is a person's work: change it in %s, then `secret accept %s %s %s`",
|
|
module, name, module, node, module, name)}
|
|
default:
|
|
return ErrNotRotatable{Why: fmt.Sprintf(
|
|
"%s does not say how it takes %q, so the mesh will not rotate it: a secret rotated under "+
|
|
"software that never reads it again is worse than one left alone. Its definition says "+
|
|
"\"own-secrets\": {%q: {\"path\": …, \"taken\": \"at-start\"}} when the module reads it as it "+
|
|
"starts, or \"applied\" when its own code applies it",
|
|
module, name, name)}
|
|
}
|
|
record, err := i.NodeByName(ctx, node)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
key, err := i.SealingKeyOf(ctx, node)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if key == "" {
|
|
return fmt.Errorf("%s has no sealing key, so nothing can be sealed to it", node)
|
|
}
|
|
var origin string
|
|
err = i.store.Pool().QueryRow(ctx,
|
|
`select origin from module_secret where node = $1 and module = $2 and name = $3`,
|
|
record.ID, module, name).Scan(&origin)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return fmt.Errorf("%s on %s holds no %q yet; the first push makes it", module, node, name)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if origin == OriginAccepted {
|
|
return ErrNotRotatable{Why: fmt.Sprintf(
|
|
"%s on %s holds %q as a value given to the mesh, not made by it, and the mesh will not "+
|
|
"replace what it cannot read (ADR 0113). Change it where it lives, then `secret accept "+
|
|
"%s %s %s` with the new value",
|
|
module, node, name, node, module, name)}
|
|
}
|
|
if len(m.ProvisionsSharing(name)) > 0 {
|
|
// Shared with every consumer of those provisions (ADR 0158): one new value, sealed to all.
|
|
return i.remakeShared(ctx, record.ID, key, module, name, "", nil, "", "", "")
|
|
}
|
|
operator, err := i.OperatorKey(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
made, blob, err := secrets.MakeWithOperator(key, key, operator)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
forOperator, operatorKey := operatorColumns(operator, blob)
|
|
_, err = i.store.Pool().Exec(ctx,
|
|
`update module_secret set sealed = $4, node_key = $5, origin = 'made', made_at = now(),
|
|
operator_sealed = $6, operator_key = $7
|
|
where node = $1 and module = $2 and name = $3`,
|
|
record.ID, module, name, made.ForConsumer, key, forOperator, operatorKey)
|
|
return err
|
|
}
|
|
|
|
// SharedSecretFor is a consumer's copy of a provider's one credential (novox/hq ADR 0158): the
|
|
// provider's own secret, sealed to this consumer as a pair credential would be.
|
|
//
|
|
// **One value, many seals, made in one act.** The mesh keeps no plaintext, so a value cannot be
|
|
// sealed to a consumer that binds later; when a consumer's copy is missing or was made in a
|
|
// different act than the provider's own secret, a fresh value is made and sealed to the provider,
|
|
// to every consumer that holds the provision from this provider, to this consumer and to the
|
|
// operator — one generation, stamped on every row. Every holding machine must then be sent, which
|
|
// the plan's caller does by sending the node it was composing and `secret rotate` does for all.
|
|
//
|
|
// An accepted value is sealed to the consumers of the moment it was accepted and never remade: a
|
|
// consumer that binds later is refused with the way out, as ADR 0113 says.
|
|
func (i *Inventory) SharedSecretFor(ctx context.Context, provision, consumer, consumerModule,
|
|
provider, providerModule, local, own string) (Secret, error) {
|
|
consumerKey, err := i.SealingKeyOf(ctx, consumer)
|
|
if err != nil {
|
|
return Secret{}, err
|
|
}
|
|
consumerNode, err := i.NodeByName(ctx, consumer)
|
|
if err != nil {
|
|
return Secret{}, err
|
|
}
|
|
providerNode, err := i.NodeByName(ctx, provider)
|
|
if err != nil {
|
|
return Secret{}, err
|
|
}
|
|
providerKey, err := i.SealingKeyOf(ctx, provider)
|
|
if err != nil {
|
|
return Secret{}, err
|
|
}
|
|
if consumerKey == "" || providerKey == "" {
|
|
return Secret{}, fmt.Errorf("%s and %s both need a sealing key before %s can be shared", consumer, provider, provision)
|
|
}
|
|
|
|
var ownGeneration, ownOrigin, ownKey *string
|
|
err = i.store.Pool().QueryRow(ctx,
|
|
`select generation, origin, node_key from module_secret where node = $1 and module = $2 and name = $3`,
|
|
providerNode.ID, providerModule, own).Scan(&ownGeneration, &ownOrigin, &ownKey)
|
|
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
|
|
return Secret{}, err
|
|
}
|
|
var held Secret
|
|
var pairGeneration *string
|
|
err = i.store.Pool().QueryRow(ctx,
|
|
`select for_consumer, for_provider, consumer_key, provider_key, origin, generation from secret
|
|
where name = $1 and consumer = $2 and consumer_module = $3 and provider = $4 and local = $5`,
|
|
provision, consumerNode.ID, consumerModule, providerNode.ID, local).
|
|
Scan(&held.ForConsumer, &held.ForProvider, &held.ConsumerKey, &held.ProviderKey, &held.Origin, &pairGeneration)
|
|
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
|
|
return Secret{}, err
|
|
}
|
|
current := ownGeneration != nil && pairGeneration != nil && *ownGeneration == *pairGeneration &&
|
|
held.ConsumerKey == consumerKey && held.ProviderKey == providerKey && ownKey != nil && *ownKey == providerKey
|
|
if current {
|
|
held.Name, held.Consumer, held.Provider = provision, consumer, provider
|
|
held.ConsumerModule, held.Local = consumerModule, local
|
|
return held, nil
|
|
}
|
|
if ownOrigin != nil && *ownOrigin == OriginAccepted {
|
|
return Secret{}, fmt.Errorf(
|
|
"%s on %s needs %s from %s, whose credential is %s's own secret %q — a value given to the "+
|
|
"mesh, which cannot seal it to a consumer that binds later (ADR 0158): `secret accept %s %s %s` "+
|
|
"again, which seals it to every current consumer",
|
|
consumerModule, consumer, provision, provider, providerModule, own, provider, providerModule, own)
|
|
}
|
|
if err := i.remakeShared(ctx, providerNode.ID, providerKey, providerModule, own, provision, consumerNode.ID, consumerKey, consumerModule, local); err != nil {
|
|
return Secret{}, err
|
|
}
|
|
return i.SharedSecretFor(ctx, provision, consumer, consumerModule, provider, providerModule, local, own)
|
|
}
|
|
|
|
// remakeShared makes one fresh value and seals it to the provider's own secret, to every pair row
|
|
// of the provisions sharing it, to the one consumer being added (when there is one), and to the
|
|
// operator, all under one generation.
|
|
func (i *Inventory) remakeShared(ctx context.Context, providerID any, providerKey, providerModule, own,
|
|
provision string, addConsumerID any, addConsumerKey, addConsumerModule, addLocal string) error {
|
|
m, err := i.declared(ctx, providerModule)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
provisions := m.ProvisionsSharing(own)
|
|
if len(provisions) == 0 {
|
|
return fmt.Errorf("%s names no provision whose credential is its own secret %q", providerModule, own)
|
|
}
|
|
operator, err := i.OperatorKey(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
value := secrets.Fresh()
|
|
generation := secrets.Stamp()
|
|
ownSealed, err := secrets.Seal(providerKey, []byte(value))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
forOperator, operatorKey := "", ""
|
|
if operator != "" {
|
|
if forOperator, err = secrets.Seal(operator, []byte(value)); err != nil {
|
|
return err
|
|
}
|
|
operatorKey = operator
|
|
}
|
|
tx, err := i.store.Pool().Begin(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = tx.Rollback(ctx) }()
|
|
if _, err := tx.Exec(ctx,
|
|
`insert into module_secret (node, module, name, sealed, node_key, origin, operator_sealed, operator_key, generation)
|
|
values ($1, $2, $3, $4, $5, 'made', nullif($6,''), nullif($7,''), $8)
|
|
on conflict (node, module, name) do update set
|
|
sealed = excluded.sealed, node_key = excluded.node_key, origin = 'made', made_at = now(),
|
|
operator_sealed = excluded.operator_sealed, operator_key = excluded.operator_key,
|
|
generation = excluded.generation`,
|
|
providerID, providerModule, own, ownSealed, providerKey, forOperator, operatorKey, generation); err != nil {
|
|
return err
|
|
}
|
|
// Every consumer that already holds one of the sharing provisions from this provider.
|
|
rows, err := tx.Query(ctx,
|
|
`select s.consumer, s.consumer_module, s.local, s.name, n.sealing_key
|
|
from secret s join node n on n.id = s.consumer
|
|
where s.provider = $1 and s.name = any($2)`, providerID, provisions)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
type holder struct {
|
|
consumer any
|
|
consumerModule, local, name, key string
|
|
}
|
|
var holders []holder
|
|
for rows.Next() {
|
|
var h holder
|
|
var key *string
|
|
if err := rows.Scan(&h.consumer, &h.consumerModule, &h.local, &h.name, &key); err != nil {
|
|
rows.Close()
|
|
return err
|
|
}
|
|
if key != nil {
|
|
h.key = *key
|
|
}
|
|
holders = append(holders, h)
|
|
}
|
|
rows.Close()
|
|
if addConsumerID != nil {
|
|
holders = append(holders, holder{consumer: addConsumerID, consumerModule: addConsumerModule,
|
|
local: addLocal, name: provision, key: addConsumerKey})
|
|
}
|
|
for _, h := range holders {
|
|
if h.key == "" {
|
|
continue // a consumer whose key is gone cannot be sealed to; it is remade when it reports one
|
|
}
|
|
sealed, err := secrets.Accept(value, h.key, providerKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := tx.Exec(ctx,
|
|
`insert into secret (name, consumer, consumer_module, provider, for_consumer, for_provider,
|
|
consumer_key, provider_key, origin, local, generation)
|
|
values ($1, $2, $3, $4, $5, $6, $7, $8, 'made', $9, $10)
|
|
on conflict (name, local, 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,
|
|
origin = 'made', generation = excluded.generation`,
|
|
h.name, h.consumer, h.consumerModule, providerID, sealed.ForConsumer, sealed.ForProvider,
|
|
h.key, providerKey, h.local, generation); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return tx.Commit(ctx)
|
|
}
|
|
|
|
// SharedHolders is every machine holding a copy of a provider's shared credential: the provider's
|
|
// and every consumer's, for the send that follows a rotation.
|
|
func (i *Inventory) SharedHolders(ctx context.Context, provider, providerModule, own string) ([]string, error) {
|
|
m, err := i.declared(ctx, providerModule)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
provisions := m.ProvisionsSharing(own)
|
|
if len(provisions) == 0 {
|
|
return nil, nil
|
|
}
|
|
providerNode, err := i.NodeByName(ctx, provider)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rows, err := i.store.Pool().Query(ctx,
|
|
`select distinct n.name from secret s join node n on n.id = s.consumer
|
|
where s.provider = $1 and s.name = any($2)`, providerNode.ID, provisions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
seen := map[string]bool{provider: true}
|
|
out := []string{provider}
|
|
for rows.Next() {
|
|
var name string
|
|
if err := rows.Scan(&name); err != nil {
|
|
return nil, err
|
|
}
|
|
if !seen[name] {
|
|
seen[name] = true
|
|
out = append(out, name)
|
|
}
|
|
}
|
|
sort.Strings(out)
|
|
return out, nil
|
|
}
|