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.
101 lines
3.6 KiB
Go
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
|
|
}
|