Files
mesh-controller/internal/inventory/operator.go
T
jschoubben 565f144a20 A pair credential is sealed to the operator key too
The secret the vault provides a module is the credential of the consumer↔vault
pair, and so is every credential a provider grants; sealing only own secrets
to the operator left exactly those unrecoverable. Same column, same call; the
export and `secret recover` address a pair by consumer node, module and the
provision's name, and say which kind each entry is.
2026-09-21 00:36:16 +02:00

131 lines
5.4 KiB
Go

package inventory
import (
"context"
"errors"
"fmt"
"github.com/jackc/pgx/v5"
"github.com/novox/mesh-controller/internal/catalogue"
)
// The operator's sealing key: the one holder of secrets that is not a node.
//
// Every secret a module holds for itself is sealed to the node that uses it, and a node whose key
// is gone takes its secrets with it — the mesh's root secrets included. novox/hq ADR 0085 (amended)
// gives them a second recipient: a person, holding a key whose private half never enters the mesh.
// What is recorded here is the public half, which is all the mesh needs to seal to it; what it
// yields is one more blob per secret that the mesh cannot open.
// OperatorKey is the public key secrets are also sealed to, or empty when the mesh has none.
func (i *Inventory) OperatorKey(ctx context.Context) (string, error) {
var key string
err := i.store.Pool().QueryRow(ctx,
`select public from operator_key order by made_at desc limit 1`).Scan(&key)
if errors.Is(err, pgx.ErrNoRows) {
return "", nil
}
return key, err
}
// SetOperatorKey records the operator's public key, replacing any earlier one.
//
// **Replacing is said, not silent.** Secrets sealed to the earlier key stay sealed to it: the
// plaintext is gone, so they cannot be sealed again to the new one until each is issued again. The
// number of them is returned so the caller can say so — a key swapped with nothing said would look
// like a mesh with a recovery path and be a mesh without one.
func (i *Inventory) SetOperatorKey(ctx context.Context, public string) (orphaned int, err error) {
if public == "" {
return 0, fmt.Errorf("an operator key is a public key, and this is nothing")
}
tx, err := i.store.Pool().Begin(ctx)
if err != nil {
return 0, err
}
defer tx.Rollback(ctx)
if err := tx.QueryRow(ctx,
`select count(*) from module_secret
where operator_key is not null and operator_key <> $1`, public).Scan(&orphaned); err != nil {
return 0, err
}
if _, err := tx.Exec(ctx, `delete from operator_key where public <> $1`, public); err != nil {
return 0, err
}
if _, err := tx.Exec(ctx,
`insert into operator_key (public) values ($1) on conflict (public) do nothing`, public); err != nil {
return 0, err
}
return orphaned, tx.Commit(ctx)
}
// Kept is the catalogue's: one secret as the operator can recover it.
type Kept = catalogue.Kept
// KeptForOperator is every secret the operator can recover, and which cannot.
//
// The second list is the honest half: a secret minted before the mesh had an operator key has no
// operator-sealed copy and cannot get one — the plaintext was discarded. Naming those is what lets
// an export say what it does not cover, rather than being taken for complete.
func (i *Inventory) KeptForOperator(ctx context.Context) (kept []Kept, unrecoverable []Kept, err error) {
rows, err := i.store.Pool().Query(ctx,
`select 'own', n.name, s.module, s.name, '', s.origin, coalesce(s.operator_sealed, ''),
coalesce(s.operator_key, ''), s.made_at
from module_secret s join node n on n.id = s.node
union all
select 'pair', c.name, s.consumer_module, s.name, p.name, 'made', coalesce(s.operator_sealed, ''),
coalesce(s.operator_key, ''), s.created_at
from secret s join node c on c.id = s.consumer join node p on p.id = s.provider
order by 1, 2, 3, 4`)
if err != nil {
return nil, nil, err
}
defer rows.Close()
for rows.Next() {
var k Kept
if err := rows.Scan(&k.Kind, &k.Node, &k.Module, &k.Name, &k.Provider, &k.Origin, &k.Sealed, &k.Key, &k.MadeAt); err != nil {
return nil, nil, err
}
if k.Sealed == "" {
unrecoverable = append(unrecoverable, k)
continue
}
kept = append(kept, k)
}
return kept, unrecoverable, rows.Err()
}
// KeptSecret is one secret's operator-sealed copy, for recovery.
func (i *Inventory) KeptSecret(ctx context.Context, node, module, name string) (Kept, error) {
// An own secret first, then a pair credential by the provision's name. A module whose own
// secret and requirement share a name is refused at resolution, so the two cannot both answer.
var k Kept
err := i.store.Pool().QueryRow(ctx,
`select 'own', n.name, s.module, s.name, '', s.origin, coalesce(s.operator_sealed, ''),
coalesce(s.operator_key, ''), s.made_at
from module_secret s join node n on n.id = s.node
where n.name = $1 and s.module = $2 and s.name = $3`, node, module, name).
Scan(&k.Kind, &k.Node, &k.Module, &k.Name, &k.Provider, &k.Origin, &k.Sealed, &k.Key, &k.MadeAt)
if errors.Is(err, pgx.ErrNoRows) {
err = i.store.Pool().QueryRow(ctx,
`select 'pair', c.name, s.consumer_module, s.name, p.name, 'made', coalesce(s.operator_sealed, ''),
coalesce(s.operator_key, ''), s.created_at
from secret s join node c on c.id = s.consumer join node p on p.id = s.provider
where c.name = $1 and s.consumer_module = $2 and s.name = $3`, node, module, name).
Scan(&k.Kind, &k.Node, &k.Module, &k.Name, &k.Provider, &k.Origin, &k.Sealed, &k.Key, &k.MadeAt)
}
if errors.Is(err, pgx.ErrNoRows) {
return Kept{}, fmt.Errorf("%s on %s holds nothing called %q — neither a secret of its own nor a credential for a provision", module, node, name)
}
if err != nil {
return Kept{}, err
}
if k.Sealed == "" {
return Kept{}, fmt.Errorf(
"%s on %s holds %q, but it was made before the mesh had an operator key and so has no "+
"copy a person can open. Issue it again (secret accept, or let the mesh remake it) "+
"and it will", module, node, name)
}
return k, nil
}