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,157 @@
|
||||
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
|
||||
}
|
||||
if _, err := os.Stat(*out); err == nil {
|
||||
return fmt.Errorf("%s already exists; this will not overwrite a key somebody may still need", *out)
|
||||
}
|
||||
public, private, err := secrets.Keypair()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(*out, []byte(private+"\n"), 0o600); 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, 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(unrecoverable) > 0 {
|
||||
fmt.Printf(" %d secret(s) not recoverable — made before it, or sealed to an earlier 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
|
||||
}
|
||||
Reference in New Issue
Block a user