A board reads through interfaces and holds nothing. Everything it needs is already answered — as text, for people, which is not something a page can read. `--json` rather than a serving API, because nothing needs one yet: whatever serves a board runs the command, and the constraint holds either way — the board never touches a context's store. An API is the larger thing and should wait until something asks for it. Both forms are gathered from the same reads before either says anything, so they answer the same questions rather than being two implementations that can drift. That was not true of the first version: the JSON printed after the text, because the branch was too late. Four properties, each asserted and each confirmed to fail when removed: - refused and failed stay distinct all the way out. They are fixed in different places, so one word for both sends half a page's readers to the wrong one — and how much DID apply is carried, since "three of eight" and "none of eight" are different machines - a machine that never spoke carries no time at all, rather than a zero one that any page would format as a date in 1970 - nothing is null. A page distinguishing "no machines are wrong" from "this field is missing" has to handle both, and null is the one that gets forgotten - no field is named like a secret. Everything here comes from records that hold no readable one, but a shape a page is built against is exactly where one would eventually be added for convenience
220 lines
7.8 KiB
Go
220 lines
7.8 KiB
Go
package inventory
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/novox/mesh-control/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
|
|
Provider string
|
|
ForConsumer string
|
|
ForProvider string
|
|
ConsumerKey string
|
|
ProviderKey string
|
|
}
|
|
|
|
// SecretFor is the credential for one provision between two nodes, making one 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, provider 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 from secret
|
|
where name = $1 and consumer = $2 and provider = $3`,
|
|
name, consumerNode.ID, providerNode.ID).
|
|
Scan(&held.ForConsumer, &held.ForProvider, &held.ConsumerKey, &held.ProviderKey)
|
|
if err == nil && held.ConsumerKey == consumerKey && held.ProviderKey == providerKey {
|
|
held.Name, held.Consumer, held.Provider = name, consumer, provider
|
|
return held, nil
|
|
}
|
|
|
|
made, err := secrets.Make(consumerKey, providerKey)
|
|
if err != nil {
|
|
return Secret{}, err
|
|
}
|
|
_, err = i.store.Pool().Exec(ctx,
|
|
`insert into secret (name, consumer, provider, for_consumer, for_provider,
|
|
consumer_key, provider_key)
|
|
values ($1, $2, $3, $4, $5, $6, $7)
|
|
on conflict (name, consumer, 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()`,
|
|
name, consumerNode.ID, providerNode.ID,
|
|
made.ForConsumer, made.ForProvider, made.ConsumerKey, made.ProviderKey)
|
|
if err != nil {
|
|
return Secret{}, err
|
|
}
|
|
return Secret{Name: name, Consumer: consumer, Provider: provider,
|
|
ForConsumer: made.ForConsumer, ForProvider: made.ForProvider,
|
|
ConsumerKey: made.ConsumerKey, ProviderKey: made.ProviderKey}, nil
|
|
}
|
|
|
|
// 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.
|
|
func (i *Inventory) RotateSecret(ctx context.Context, name, consumer, provider string) error {
|
|
consumerNode, err := i.NodeByName(ctx, consumer)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
providerNode, err := i.NodeByName(ctx, provider)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = i.store.Pool().Exec(ctx,
|
|
`delete from secret where name = $1 and consumer = $2 and provider = $3`,
|
|
name, consumerNode.ID, providerNode.ID)
|
|
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.for_provider from secret s
|
|
join node c on c.id = s.consumer
|
|
where s.provider = $1 order by s.name, c.name`, 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.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.
|
|
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 string
|
|
err = i.store.Pool().QueryRow(ctx,
|
|
`select sealed, node_key from module_secret where node = $1 and module = $2 and name = $3`,
|
|
record.ID, module, name).Scan(&sealed, &against)
|
|
if err == nil && against == key {
|
|
return sealed, nil
|
|
}
|
|
|
|
made, err := secrets.Make(key, key)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
// Sealed once, to one recipient. Make seals to two ends because a provision has two; here
|
|
// both are the same machine, and only one copy is kept.
|
|
if _, err := i.store.Pool().Exec(ctx,
|
|
`insert into module_secret (node, module, name, sealed, node_key)
|
|
values ($1, $2, $3, $4, $5)
|
|
on conflict (node, module, name) do update set
|
|
sealed = excluded.sealed, node_key = excluded.node_key, made_at = now()`,
|
|
record.ID, module, name, made.ForConsumer, key); 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 {
|
|
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
|
|
}
|
|
_, err = i.store.Pool().Exec(ctx,
|
|
`insert into module_secret (node, module, name, sealed, node_key)
|
|
values ($1, $2, $3, $4, $5)
|
|
on conflict (node, module, name) do update set
|
|
sealed = excluded.sealed, node_key = excluded.node_key, made_at = now()`,
|
|
record.ID, module, name, sealed.ForConsumer, key)
|
|
return err
|
|
}
|