Files
mesh-controller/internal/inventory/secret_move.go
T
mesh-tools 97a2d841c7 Land feature 57's controller work on its release branch
The controller plans config, templates and moved providers' provisions,
hands bus accounts to the vault and judges the new manifest words, on the
feature's SDK and node-engine validator (feature 57, config and provisioning).
2026-10-15 21:27:33 +00:00

219 lines
8.2 KiB
Go

package inventory
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"time"
"github.com/jackc/pgx/v5"
)
// The move of the controller's secrets into the vault (novox/hq ADR 0349 clause 7; design 57's V3, issue 676; the
// operator's decision of 2026-10-14 on that issue).
//
// The controller opens none of the values it holds. Each row is moved by the node-engine of a machine it was sealed
// to: a module's own secret by its machine's, a pair credential by either end's. This is what the controller reads to
// ask each machine — every row sealed to it, as the store holds it — and where it keeps what moved. Nothing here
// opens, makes or changes a value: the rows are read as they are.
// SecretToMove is one row of the controller's store as the move asks a machine to hand it in.
type SecretToMove struct {
// Secret is the vault's name for the row (VaultNameOfOwn, VaultNameOfPair).
Secret string
// Node is the machine asked: the module's machine for an own secret, the end being asked for a pair.
Node string
// Module and Name are an own secret's; empty for a pair.
Module, Name string
// Provider, Provision, Consumer, ConsumerModule and Local are a pair credential's; empty for an own secret.
Provider, Provision, Consumer, ConsumerModule, Local string
Origin string
// Generation is the controller's stamp of the act that made the value (ADR 0158, ADR 0305 §5); empty for none.
Generation string
MadeAt time.Time
// Sealed is the value sealed to Node, and SealedTo the key the row says it was sealed to.
Sealed, SealedTo string
// Current says SealedTo is Node's sealing key now: only then can its engine open it.
Current bool
// Digest is the digest of the row's sealed text as the store holds it — both ends' for a pair, so either end
// reads the same — by which a row the controller changed after it moved is told from one that did not.
Digest string
// Moved is the record of the row having moved, by either end, or nil.
Moved *SecretMoved
}
// Own says the row is a module's own secret.
func (s SecretToMove) Own() bool { return s.Module != "" }
// SealedDigest is sha256 of a row's sealed texts, each on its own line, in hex: nothing of the value is kept.
func SealedDigest(sealed ...string) string {
h := sha256.New()
for _, s := range sealed {
h.Write([]byte(s + "\n"))
}
return "sha256:" + hex.EncodeToString(h.Sum(nil))
}
// SecretMoved is a row the vault took: which machine's engine handed it in, the vault's generation, the digest of the
// sealed text it was moved from, and when.
type SecretMoved struct {
Node string
Generation int
Digest string
At time.Time
}
// VaultNameOfOwn is the vault's name for a module's own secret (mesh-vault OwnSecret).
func VaultNameOfOwn(node, module, name string) string {
return "own/" + node + "/" + module + "/" + name
}
// VaultNameOfPair is the vault's name for one row of the pair credentials (mesh-vault PairSecret): both ends, the
// consuming module, and the local name last when it has one.
func VaultNameOfPair(provider, provision, consumer, consumerModule, local string) string {
name := "pair/" + provider + "/" + provision + "/" + consumer + "/" + consumerModule
if local != "" {
name += "/" + local
}
return name
}
// SecretsToMoveFrom is every row sealed to a machine, as the store holds it: its modules' own secrets, and each pair
// credential it is an end of, sealed to it; each with whether its key is the machine's now and whether it moved.
func (i *Inventory) SecretsToMoveFrom(ctx context.Context, node string) ([]SecretToMove, error) {
record, err := i.NodeByName(ctx, node)
if err != nil {
return nil, err
}
current, err := i.SealingKeyOf(ctx, node)
if err != nil {
return nil, err
}
moved, err := i.secretsMoved(ctx)
if err != nil {
return nil, err
}
var out []SecretToMove
rows, err := i.store.Pool().Query(ctx,
`select module, name, origin, coalesce(generation, ''), made_at, sealed, node_key from module_secret
where node = $1 order by module, name`, record.ID)
if err != nil {
return nil, err
}
for rows.Next() {
s := SecretToMove{Node: node}
if err := rows.Scan(&s.Module, &s.Name, &s.Origin, &s.Generation, &s.MadeAt, &s.Sealed, &s.SealedTo); err != nil {
rows.Close()
return nil, err
}
s.Secret, s.Digest = VaultNameOfOwn(node, s.Module, s.Name), SealedDigest(s.Sealed)
out = append(out, s)
}
rows.Close()
if err := rows.Err(); err != nil {
return nil, err
}
rows, err = i.store.Pool().Query(ctx,
`select p.name, s.name, c.name, s.consumer_module, s.local, s.origin, coalesce(s.generation, ''), s.created_at,
case when s.consumer = $1 then s.for_consumer else s.for_provider end,
case when s.consumer = $1 then s.consumer_key else s.provider_key end,
s.for_consumer, s.for_provider
from secret s join node p on p.id = s.provider join node c on c.id = s.consumer
where s.consumer = $1 or s.provider = $1
order by p.name, s.name, c.name, s.consumer_module, s.local`, record.ID)
if err != nil {
return nil, err
}
for rows.Next() {
s := SecretToMove{Node: node}
var forConsumer, forProvider string
if err := rows.Scan(&s.Provider, &s.Provision, &s.Consumer, &s.ConsumerModule, &s.Local, &s.Origin,
&s.Generation, &s.MadeAt, &s.Sealed, &s.SealedTo, &forConsumer, &forProvider); err != nil {
rows.Close()
return nil, err
}
s.Digest = SealedDigest(forConsumer, forProvider)
s.Secret = VaultNameOfPair(s.Provider, s.Provision, s.Consumer, s.ConsumerModule, s.Local)
out = append(out, s)
}
rows.Close()
if err := rows.Err(); err != nil {
return nil, err
}
for n := range out {
out[n].Current = current != "" && out[n].SealedTo == current
if m, ok := moved[out[n].Secret]; ok {
out[n].Moved = &m
}
}
return out, nil
}
func (i *Inventory) secretsMoved(ctx context.Context) (map[string]SecretMoved, error) {
rows, err := i.store.Pool().Query(ctx,
`select m.secret, n.name, m.generation, m.sealed_digest, m.moved_at from secret_moved m join node n on n.id = m.node`)
if err != nil {
return nil, err
}
defer rows.Close()
out := map[string]SecretMoved{}
for rows.Next() {
var name string
var m SecretMoved
if err := rows.Scan(&name, &m.Node, &m.Generation, &m.Digest, &m.At); err != nil {
return nil, err
}
out[name] = m
}
return out, rows.Err()
}
// RecordSecretMoved keeps that the vault took a row, read back the same, from a machine's engine. Kept once: the
// first record of a row stands, so the other end of a pair moving it again changes nothing.
func (i *Inventory) RecordSecretMoved(ctx context.Context, s SecretToMove, generation int) error {
record, err := i.NodeByName(ctx, s.Node)
if err != nil {
return err
}
_, err = i.store.Pool().Exec(ctx,
`insert into secret_moved (secret, node, generation, sealed_digest) values ($1, $2, $3, $4)
on conflict (secret) do nothing`, s.Secret, record.ID, generation, s.Digest)
return err
}
// SecretMove is the move as started: when, by whom, and when the record of asks was copied.
type SecretMove struct {
StartedAt time.Time
StartedBy string
AsksMovedAt *time.Time
}
// StartSecretMove records that the move started, the first time; later starts keep the first. It answers the move
// as recorded.
func (i *Inventory) StartSecretMove(ctx context.Context, by string) (SecretMove, error) {
if _, err := i.store.Pool().Exec(ctx,
`insert into secret_move (started_by) values ($1) on conflict (one) do nothing`, by); err != nil {
return SecretMove{}, err
}
m, _, err := i.SecretMoveStarted(ctx)
return m, err
}
// SecretMoveStarted is the move as recorded, and whether it was started: a machine heard from is moved only then.
func (i *Inventory) SecretMoveStarted(ctx context.Context) (SecretMove, bool, error) {
var m SecretMove
err := i.store.Pool().QueryRow(ctx, `select started_at, started_by, asks_moved_at from secret_move`).
Scan(&m.StartedAt, &m.StartedBy, &m.AsksMovedAt)
if errors.Is(err, pgx.ErrNoRows) {
return SecretMove{}, false, nil
}
return m, err == nil, err
}
// RecordAsksMoved keeps that the record of asks was copied into the vault.
func (i *Inventory) RecordAsksMoved(ctx context.Context) error {
_, err := i.store.Pool().Exec(ctx, `update secret_move set asks_moved_at = now() where asks_moved_at is null`)
return err
}