Files
mesh-host/internal/bootstrap/operator.go
T
jschoubben 70d0f36896 Installer review: secrets are staged privately, and a bundle is 0600 whether or not it existed
From review: the store and broker passwords genesis makes were carried into
the controller through a world-readable file in /tmp, a bundle left at 0644 by
an earlier installer kept that mode while now holding them, a mesh raised by
the old installer would have been handed new passwords its servers do not have,
and the broker-admin action's marker did not depend on the value. Secrets now
stage in a 0700 directory owned by the controller's account; the bundle is
chmod'd; an existing store or broker volume with no credential file is refused
by name; the marker holds the password's fingerprint. Also: one install path
for the store, broker and vault, no error-string matching for the operator
key, and no unreachable fallback for the superuser.
2026-09-21 01:26:35 +02:00

101 lines
3.6 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)}
var key identity.SealingKey
if _, err := os.Stat(out.Path); err == nil {
key, err = identity.LoadSealingKey(out.Path)
if err != nil {
return out, err
}
say(" operator key already at " + out.Path + " — kept")
} else if os.IsNotExist(err) {
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
} else {
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
}
// 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
}