Files
mesh-host/internal/bootstrap/operator.go
T
jschoubben ee0c8b856e Genesis makes the root secrets, the operator key, and installs the vault
The template raises the store with the password 'bootstrap' and the broker
with its image's default administrator, and the installer carried both into
the mesh as accepted secrets — permanent, and not secret (novox/hq issue 071).

Now the installer makes both credentials, once, at the paths the postgres and
lavinmq modules declare as their own secrets, rewrites the produced bundle to
use them (the store reads its password from a file; the broker's default
account is given the new password by an action before anything dials it), and
writes the bundle at 0600 since it now carries them.

Before the first secret is accepted it makes the operator's sealing key beside
the bundle and gives the mesh the public half, so everything minted from there
is sealed to it too (ADR 0085, amended). Phase three adopts the broker as the
lavinmq module beside the store and installs mesh-vault as a foundation module;
the run ends by writing the operator-sealed export beside the key.
2026-09-21 00:12:55 +02:00

108 lines
3.7 KiB
Go

package bootstrap
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/novox/mesh-host/internal/identity"
)
// The operator's sealing key: made at genesis, before the mesh is told any secret.
//
// Every secret a module holds for itself is sealed to the node that uses it; from here on it is
// sealed to this key as well, and the vault keeps those copies (novox/hq ADR 0085, amended). The
// private half is written once, beside the produced bundle, and given to nothing: the mesh
// records the public half and can open nothing it seals to it. The operator copies the file off
// the machine and keeps it — it is what recovers the mesh's root secrets when a node cannot.
//
// **Before enrolment's first `secret accept`**, or the credentials genesis made would be sealed
// to the node alone and be exactly as unrecoverable as the constants they replaced.
// OperatorKeyFile is where the private half is written, beside the bundle.
func OperatorKeyFile(o Options) string {
return filepath.Join(filepath.Dir(o.Out), "operator.key")
}
// RootExportFile is where the export of every operator-sealed secret is written at the end.
func RootExportFile(o Options) string {
return filepath.Join(filepath.Dir(o.Out), "root-secrets.export.json")
}
type OperatorKey struct {
Path string
Fingerprint string
Made bool
}
// MakeOperatorKey makes the key if this machine has none, and tells the mesh its public half.
func MakeOperatorKey(ctx context.Context, o Options, control controlPlane, say func(string)) (OperatorKey, error) {
out := OperatorKey{Path: OperatorKeyFile(o)}
key, err := identity.LoadSealingKey(out.Path)
switch {
case err == nil:
say(" operator key already at " + out.Path + " — kept")
case os.IsNotExist(underlying(err)) || strings.Contains(err.Error(), "no sealing key at"):
key, err = identity.GenerateSealingKey()
if err != nil {
return out, err
}
if err := os.MkdirAll(filepath.Dir(out.Path), 0o755); err != nil {
return out, err
}
if err := os.WriteFile(out.Path, []byte(key.Private+"\n"), 0o600); err != nil {
return out, err
}
out.Made = true
default:
return out, err
}
sum := sha256.Sum256([]byte(key.Public))
out.Fingerprint = "sha256:" + hex.EncodeToString(sum[:8])
if _, err := control.tell(ctx, "operator", "key", "set", key.Public); err != nil {
return out, err
}
if out.Made {
say(" operator key " + out.Fingerprint + " — private half at " + out.Path + " (0600)")
say(" COPY IT OFF THIS MACHINE AND KEEP IT: it opens the mesh's root secrets, and")
say(" nothing else does. The mesh holds only the public half.")
} else {
say(" operator key " + out.Fingerprint + " — the mesh seals its root secrets to it")
}
return out, nil
}
func underlying(err error) error {
for {
next, ok := err.(interface{ Unwrap() error })
if !ok || next.Unwrap() == nil {
return err
}
err = next.Unwrap()
}
}
// ExportRootSecrets writes the export beside the operator key: every secret sealed to it, as
// ciphertext, and the honest list of what is not. What the vault keeps on its disk, kept once
// more by the person who holds the key.
func ExportRootSecrets(ctx context.Context, o Options, control controlPlane, say func(string)) (string, error) {
path := RootExportFile(o)
body, err := control.tell(ctx, "secret", "export")
if err != nil {
return path, err
}
if !strings.Contains(body, `"kept"`) {
return path, fmt.Errorf("`secret export` did not produce an export:\n%s", body)
}
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
return path, err
}
say(" exported " + path + " (0600) — ciphertext, sealed to the operator key; keep it with the key")
return path, nil
}