An operator key, a second seal on every own secret, and the vault keeps the export
novox/hq ADR 0085, amended: the mesh's root secrets — the store's superuser, the broker's administrator, every secret a module holds for itself — were sealed to a node key and nothing else, so a lost node took them with it. Now the mesh records an operator's public sealing key and seals every own secret to it as well, minted or accepted. The private half is written once by `operator key new` to a file the operator keeps off the mesh; the mesh holds one more blob per secret that it cannot open. `secret recover` opens a secret with that key, to a 0600 file, from the store or from an export; `secret export` writes every operator-sealed copy as ciphertext. A module that `keeps` (the vault) is handed that export as a declared file on its own disk, so recovery survives the store. Secrets made before the key exists have no operator copy and are said so — the plaintext was discarded — until each is issued again.
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
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 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
|
||||
order by n.name, s.module, s.name`)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var k Kept
|
||||
if err := rows.Scan(&k.Node, &k.Module, &k.Name, &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) {
|
||||
var k Kept
|
||||
err := i.store.Pool().QueryRow(ctx,
|
||||
`select 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.Node, &k.Module, &k.Name, &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", 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
|
||||
}
|
||||
Reference in New Issue
Block a user