Files
mesh-controller/cmd/mesh-controller/operator.go
T
jschoubben 77e6c1a684 Recoverable means sealed to the current operator key; recovery names the provider
From review: the export counted any operator-sealed row as recoverable, so a
secret sealed to a replaced key was reported as openable with the current one;
replacing the key counted orphans in one table of two; and a pair credential
held from two providers was recovered as whichever row came first. The export
now lists what the current key opens, what an earlier key opens, and what has
no copy; `secret recover` takes --provider and refuses ambiguity; files that
must not exist are created exclusively; one constructor builds the export for
the operator's file and the vault's disk alike.
2026-09-21 01:16:32 +02:00

163 lines
5.7 KiB
Go

package main
import (
"context"
"errors"
"flag"
"fmt"
"os"
"strings"
"github.com/novox/mesh-controller/internal/secrets"
)
// operatorCommand is the mesh's one holder of secrets that is not a machine.
//
// **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 store's superuser and the broker's administrator among
// them. novox/hq ADR 0085 (amended) gives them a second recipient: a person, with a sealing key
// whose private half is made where the operator is and never enters the mesh. Making the key and
// telling the mesh about it are two commands, on purpose: the first needs no mesh at all and runs
// wherever the operator keeps things; the second gives the mesh the public half and nothing else.
// The controller's own container is a scratch image with no writable path, which is the right
// shape for a program that must hold no key — so the private half could not be written there
// even by mistake.
//
// operator key make [--out <file>] make a keypair: private half to the file, public half printed
// operator key set <public> tell the mesh which key to seal to
// operator key show the public key, its fingerprint, and what it can recover
const operatorUsage = "operator key make [--out <file>] | operator key set <public> [--replace] | operator key show"
func operatorCommand(ctx context.Context, args []string) error {
if len(args) < 2 || args[0] != "key" {
return errors.New(operatorUsage)
}
switch args[1] {
case "make":
return operatorKeyMake(args[2:])
case "set":
return operatorKeySet(ctx, args[2:])
case "show":
return operatorKeyShow(ctx)
default:
return errors.New(operatorUsage)
}
}
// operatorKeyMake needs no mesh: it is the operator's, run wherever the key will live.
func operatorKeyMake(args []string) error {
set := flag.NewFlagSet("operator key make", flag.ContinueOnError)
out := set.String("out", "operator.key",
"where to write the private key (0600); keep it off the mesh, and keep it")
if err := set.Parse(args); err != nil {
return err
}
public, private, err := secrets.Keypair()
if err != nil {
return err
}
// Create-exclusive: a key somebody may still need is never overwritten, and there is no window
// between checking and writing in which one could appear.
if err := writeNew(*out, []byte(private+"\n")); err != nil {
return err
}
fmt.Printf("operator key %s\n", secrets.Fingerprint(public))
fmt.Printf(" private half written to %s (0600) — keep it off the mesh, and keep it\n", *out)
fmt.Printf(" public half, to give the mesh with `operator key set`:\n")
fmt.Printf("public %s\n", public)
return nil
}
func operatorKeySet(ctx context.Context, args []string) error {
rest, flags := split(args)
set := flag.NewFlagSet("operator key set", flag.ContinueOnError)
replace := set.Bool("replace", false,
"replace an existing operator key — secrets sealed to the old one stay sealed to it")
if err := set.Parse(flags); err != nil {
return err
}
if len(rest) != 1 {
return errors.New(operatorUsage)
}
public := strings.TrimSpace(rest[0])
if _, err := secrets.Seal(public, []byte("probe")); err != nil {
return fmt.Errorf("that is not a public sealing key: %w", err)
}
open, err := openStores(ctx)
if err != nil {
return err
}
defer open.Close()
inv := open.inventory
if current, err := inv.OperatorKey(ctx); err != nil {
return err
} else if current != "" && current != public && !*replace {
return fmt.Errorf(
"the mesh already has an operator key (%s). Pass --replace to change it — "+
"secrets sealed to the current key stay sealed to it until each is issued again",
secrets.Fingerprint(current))
}
orphaned, err := inv.SetOperatorKey(ctx, public)
if err != nil {
return err
}
fmt.Printf("operator key %s\n", secrets.Fingerprint(public))
fmt.Printf(" the mesh holds the public half only and cannot open what it seals to it;\n")
fmt.Printf(" from now on every secret a module holds for itself is sealed to it as well.\n")
fmt.Printf(" Secrets made before this cannot be — each is recoverable once issued again\n")
if orphaned > 0 {
fmt.Printf(" %d secret(s) are sealed to the previous key and stay so until issued again\n", orphaned)
}
return nil
}
func operatorKeyShow(ctx context.Context) error {
open, err := openStores(ctx)
if err != nil {
return err
}
defer open.Close()
inv := open.inventory
key, err := inv.OperatorKey(ctx)
if err != nil {
return err
}
if key == "" {
fmt.Println("the mesh has no operator key; `operator key make` then `operator key set` gives it one")
return nil
}
kept, earlier, unrecoverable, err := inv.KeptForOperator(ctx)
if err != nil {
return err
}
fmt.Printf("operator key %s\n %s\n", secrets.Fingerprint(key), key)
fmt.Printf(" %d secret(s) recoverable with it\n", len(kept))
if len(earlier) > 0 {
fmt.Printf(" %d secret(s) sealed to an earlier operator key — recoverable with that key only, until issued again:\n", len(earlier))
for _, k := range earlier {
fmt.Printf(" %s %s %s (%s)\n", k.Node, k.Module, k.Name, secrets.Fingerprint(k.Key))
}
}
if len(unrecoverable) > 0 {
fmt.Printf(" %d secret(s) not recoverable — made before the mesh had an operator key:\n", len(unrecoverable))
for _, k := range unrecoverable {
fmt.Printf(" %s %s %s\n", k.Node, k.Module, k.Name)
}
}
return nil
}
// readPrivateKey is the operator's key from the file `operator key make` wrote.
func readPrivateKey(path string) (string, error) {
if path == "" {
return "", errors.New("--key <file> names the operator's private key, written by `operator key make`")
}
raw, err := os.ReadFile(path)
if err != nil {
return "", err
}
return strings.TrimSpace(string(raw)), nil
}