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.
This commit is contained in:
2026-09-21 01:26:35 +02:00
parent 036af3cfdc
commit 70d0f36896
8 changed files with 175 additions and 155 deletions
+5 -2
View File
@@ -394,6 +394,9 @@ func Run(ctx context.Context, o Options, d Deps, say func(string)) (Result, erro
if err != nil { if err != nil {
return result, failed(StepBundle, err) return result, failed(StepBundle, err)
} }
if err := RefuseExistingServers(ctx, d.Run, creds); err != nil {
return result, failed(StepBundle, err)
}
root, err := RewriteRoot(&rewritten, creds) root, err := RewriteRoot(&rewritten, creds)
if err != nil { if err != nil {
return result, failed(StepBundle, err) return result, failed(StepBundle, err)
@@ -644,13 +647,13 @@ func Run(ctx context.Context, o Options, d Deps, say func(string)) (Result, erro
return result, failed(StepStore, err) return result, failed(StepStore, err)
} }
// ---- 14b. broker ---------------------------------------------------------------------- // ---- broker ---------------------------------------------------------------------------
say("broker — the foundation's broker, adopted as the lavinmq module: one bus, not two") say("broker — the foundation's broker, adopted as the lavinmq module: one bus, not two")
if err := InstallBroker(ctx, o, permanentControl, rewritten.Declaration, say); err != nil { if err := InstallBroker(ctx, o, permanentControl, rewritten.Declaration, say); err != nil {
return result, failed(StepBroker, err) return result, failed(StepBroker, err)
} }
// ---- 14c. vault ----------------------------------------------------------------------- // ---- vault ----------------------------------------------------------------------------
// A foundation module (novox/hq ADR 0085, amended): it keeps every operator-sealed secret on // A foundation module (novox/hq ADR 0085, amended): it keeps every operator-sealed secret on
// its own disk, outside the store, from the first push that carries one. // its own disk, outside the store, from the first push that carries one.
say("vault — the mesh's secrets get an owner, and their operator-sealed copies a place to live") say("vault — the mesh's secrets get an owner, and their operator-sealed copies a place to live")
+1 -1
View File
@@ -274,7 +274,7 @@ func deliverStores(ctx context.Context, o Options, control controlPlane, manifes
// installer has neither a terminal to be prompted at nor a way to write to a command's // installer has neither a terminal to be prompted at nor a way to write to a command's
// standard input through the runner every applier in this repository shares. // standard input through the runner every applier in this repository shares.
at := "/accepting-" + secret at := "/accepting-" + secret
if err := control.carrying(ctx, "mesh-accepting-"+secret, []byte(value), at); err != nil { if err := control.carryingSecret(ctx, "mesh-accepting-"+secret, []byte(value), at); err != nil {
return delivered, err return delivered, err
} }
if _, err := control.tell(ctx, "secret", "accept", o.Node, ControlPlaneModule, secret, if _, err := control.tell(ctx, "secret", "accept", o.Node, ControlPlaneModule, secret,
+8 -15
View File
@@ -42,11 +42,14 @@ type OperatorKey struct {
// MakeOperatorKey makes the key if this machine has none, and tells the mesh its public half. // 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) { func MakeOperatorKey(ctx context.Context, o Options, control controlPlane, say func(string)) (OperatorKey, error) {
out := OperatorKey{Path: OperatorKeyFile(o)} out := OperatorKey{Path: OperatorKeyFile(o)}
key, err := identity.LoadSealingKey(out.Path) var key identity.SealingKey
switch { if _, err := os.Stat(out.Path); err == nil {
case err == nil: key, err = identity.LoadSealingKey(out.Path)
if err != nil {
return out, err
}
say(" operator key already at " + out.Path + " — kept") say(" operator key already at " + out.Path + " — kept")
case os.IsNotExist(underlying(err)) || strings.Contains(err.Error(), "no sealing key at"): } else if os.IsNotExist(err) {
key, err = identity.GenerateSealingKey() key, err = identity.GenerateSealingKey()
if err != nil { if err != nil {
return out, err return out, err
@@ -58,7 +61,7 @@ func MakeOperatorKey(ctx context.Context, o Options, control controlPlane, say f
return out, err return out, err
} }
out.Made = true out.Made = true
default: } else {
return out, err return out, err
} }
sum := sha256.Sum256([]byte(key.Public)) sum := sha256.Sum256([]byte(key.Public))
@@ -77,16 +80,6 @@ func MakeOperatorKey(ctx context.Context, o Options, control controlPlane, say f
return out, nil 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 // 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 // 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. // more by the person who holds the key.
+54 -121
View File
@@ -4,7 +4,6 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os"
"strings" "strings"
"github.com/novox/mesh-host/internal/declaration" "github.com/novox/mesh-host/internal/declaration"
@@ -73,6 +72,27 @@ func InstallStore(ctx context.Context, o Options, control controlPlane,
} }
say(" adopting " + store.Name + " — the store the foundation raised, unchanged") say(" adopting " + store.Name + " — the store the foundation raised, unchanged")
// The superuser is the foundation's, made at genesis — carried in before the push, or the push
// would seal random bytes where a working password has to be and the provisioner would not open
// the store it is meant to manage.
if err := installProvider(ctx, o, control, module, manifest, "(the provisioner; the server is adopted, not built)",
func() error {
return deliverCredential(ctx, o, control, module, "superuser", StoreSuperuserFile, "the store's superuser", say)
},
say); err != nil {
return err
}
say(" adopted mesh-store — the foundation's store is now the " + module + " module")
return nil
}
// installProvider registers, builds, issues, assigns and pushes one catalogue module, with one
// thing done just before the push — the moment a credential the mesh could not have made has to
// be in it. InstallFromCatalogue is the same sequence without that moment; the store, the broker
// and the vault each need it or need the shape, and three copies of it drifted.
func installProvider(ctx context.Context, o Options, control controlPlane, module string,
manifest []byte, buildNote string, beforePush func() error, say func(string)) error {
remote := "/" + module + "-module.json" remote := "/" + module + "-module.json"
if err := control.carrying(ctx, module+"-module.json", manifest, remote); err != nil { if err := control.carrying(ctx, module+"-module.json", manifest, remote); err != nil {
return err return err
@@ -87,7 +107,7 @@ func InstallStore(ctx context.Context, o Options, control controlPlane,
"the catalogue CHECKOUT says what it is, the catalogue REPOSITORY is where a builder "+ "the catalogue CHECKOUT says what it is, the catalogue REPOSITORY is where a builder "+
"clones it", module) "clones it", module)
} }
say(" building " + module + " (the provisioner; the server is adopted, not built)") say(strings.TrimRight(" building "+module+" "+buildNote, " "))
if _, err := control.within(buildWait).tell(ctx, "build", o.CatalogSource.Repository, if _, err := control.within(buildWait).tell(ctx, "build", o.CatalogSource.Repository,
"--path", "modules/"+module, "--ref", refOr(o.CatalogSource.Ref), "--wait", "1200s"); err != nil { "--path", "modules/"+module, "--ref", refOr(o.CatalogSource.Ref), "--wait", "1200s"); err != nil {
return err return err
@@ -102,37 +122,40 @@ func InstallStore(ctx context.Context, o Options, control controlPlane,
if _, err := control.tell(ctx, "assign", o.Node, module); err != nil { if _, err := control.tell(ctx, "assign", o.Node, module); err != nil {
return err return err
} }
if beforePush != nil {
// The superuser is the foundation's, made at genesis — carried in before the push, or the push if err := beforePush(); err != nil {
// would seal random bytes where a working password has to be and the provisioner would not open return err
// the store it is meant to manage. }
if err := deliverSuperuser(ctx, o, control, module, store, say); err != nil {
return err
} }
_, err := pushNode(ctx, o, control, say)
if _, err := pushNode(ctx, o, control, say); err != nil { return err
return err
}
say(" adopted mesh-store — the foundation's store is now the " + module + " module")
return nil
} }
func readCredentialFile(path string) (string, error) { // deliverCredential carries a credential genesis made into a module as its own secret, through
raw, err := os.ReadFile(path) // `secret accept`: the mesh cannot invent the value a running server already has.
func deliverCredential(ctx context.Context, o Options, control controlPlane, module, secret, file, what string,
say func(string)) error {
value, err := readCredentialFile(file)
if err != nil { if err != nil {
return "", err return fmt.Errorf("%s is not at %s, so the %s module has nothing to open its server with — "+
"and the mesh cannot invent the one the server already has: %w", what, file, module, err)
} }
value := strings.TrimRight(string(raw), "\r\n") at := "/accepting-" + secret
if value == "" { if err := control.carryingSecret(ctx, "mesh-accepting-"+secret, []byte(value), at); err != nil {
return "", fmt.Errorf("%s is empty", path) return err
} }
return value, nil if _, err := control.tell(ctx, "secret", "accept", o.Node, module, secret, "--from", at); err != nil {
return err
}
say(" accepted " + secret + " — " + what + ", as genesis made it")
return nil
} }
// InstallBroker makes the foundation's broker the `lavinmq` module, adopted in place — the same // InstallBroker makes the foundation's broker the `lavinmq` module, adopted in place — the same
// shape as InstallStore, for the same reasons. The administrator's password is the one genesis // shape as InstallStore, for the same reasons. The administrator's password is the one genesis
// gave the image's default account (rootsecrets.go), carried in through `secret accept` so the // gave the image's default account (rootsecrets.go), carried in so the module's provisioner can
// module's provisioner can reach the management API as it. // reach the management API as it.
func InstallBroker(ctx context.Context, o Options, control controlPlane, func InstallBroker(ctx context.Context, o Options, control controlPlane,
foundation *declaration.Declaration, say func(string)) error { foundation *declaration.Declaration, say func(string)) error {
@@ -149,46 +172,11 @@ func InstallBroker(ctx context.Context, o Options, control controlPlane,
return err return err
} }
say(" adopting " + broker.Name + " — the broker the foundation raised, unchanged") say(" adopting " + broker.Name + " — the broker the foundation raised, unchanged")
if err := installProvider(ctx, o, control, module, manifest, "(the provisioner; the server is adopted, not built)",
remote := "/" + module + "-module.json" func() error {
if err := control.carrying(ctx, module+"-module.json", manifest, remote); err != nil { return deliverCredential(ctx, o, control, module, "admin", BrokerAdminFile, "the broker's administrator", say)
return err },
} say); err != nil {
if _, err := control.tell(ctx, "module", "add", remote); err != nil {
return err
}
say(" registered " + module)
if o.CatalogSource.Repository == "" {
return fmt.Errorf("%s has to be built and there is no --catalog-source to build it from", module)
}
say(" building " + module + " (the provisioner; the server is adopted, not built)")
if _, err := control.within(buildWait).tell(ctx, "build", o.CatalogSource.Repository,
"--path", "modules/"+module, "--ref", refOr(o.CatalogSource.Ref), "--wait", "1200s"); err != nil {
return err
}
if _, err := control.tell(ctx, "module", "issue", module, "--node", o.Node); err != nil {
say(" no account " + module + " — it declares nothing to say on the broker")
} else {
say(" account issued " + module)
}
if _, err := control.tell(ctx, "assign", o.Node, module); err != nil {
return err
}
value, err := readCredentialFile(BrokerAdminFile)
if err != nil {
return fmt.Errorf("the broker's administrator password genesis made is not at %s: %w", BrokerAdminFile, err)
}
at := "/accepting-admin"
if err := control.carrying(ctx, "mesh-accepting-admin", []byte(value), at); err != nil {
return err
}
if _, err := control.tell(ctx, "secret", "accept", o.Node, module, "admin", "--from", at); err != nil {
return err
}
say(" accepted admin — the broker's administrator, as genesis made it")
if _, err := pushNode(ctx, o, control, say); err != nil {
return err return err
} }
say(" adopted " + broker.Name + " — the foundation's broker is now the " + module + " module") say(" adopted " + broker.Name + " — the foundation's broker is now the " + module + " module")
@@ -196,38 +184,15 @@ func InstallBroker(ctx context.Context, o Options, control controlPlane,
} }
// InstallVault installs the vault as a foundation module (novox/hq ADR 0085, amended). Nothing to // InstallVault installs the vault as a foundation module (novox/hq ADR 0085, amended). Nothing to
// adopt: it is its own runtime, built from the catalogue like any provider, and from its first push // adopt and nothing to carry in: it is its own runtime, built from the catalogue like any provider,
// it keeps the export of every operator-sealed secret on its own disk. // and from its first push it keeps the export of every operator-sealed secret on its own disk.
func InstallVault(ctx context.Context, o Options, control controlPlane, say func(string)) error { func InstallVault(ctx context.Context, o Options, control controlPlane, say func(string)) error {
const module = "mesh-vault" const module = "mesh-vault"
manifest, err := readManifest(o.Catalogue, module) manifest, err := readManifest(o.Catalogue, module)
if err != nil { if err != nil {
return err return err
} }
remote := "/" + module + "-module.json" if err := installProvider(ctx, o, control, module, manifest, "", nil, say); err != nil {
if err := control.carrying(ctx, module+"-module.json", manifest, remote); err != nil {
return err
}
if _, err := control.tell(ctx, "module", "add", remote); err != nil {
return err
}
say(" registered " + module)
if o.CatalogSource.Repository == "" {
return fmt.Errorf("%s has to be built and there is no --catalog-source to build it from", module)
}
say(" building " + module)
if _, err := control.within(buildWait).tell(ctx, "build", o.CatalogSource.Repository,
"--path", "modules/"+module, "--ref", refOr(o.CatalogSource.Ref), "--wait", "1200s"); err != nil {
return err
}
if _, err := control.tell(ctx, "module", "issue", module, "--node", o.Node); err != nil {
return err
}
say(" account issued " + module)
if _, err := control.tell(ctx, "assign", o.Node, module); err != nil {
return err
}
if _, err := pushNode(ctx, o, control, say); err != nil {
return err return err
} }
say(" installed " + module + " — the operator-sealed export now lives on this machine, outside the store") say(" installed " + module + " — the operator-sealed export now lives on this machine, outside the store")
@@ -294,35 +259,3 @@ func serverMatchesFoundation(manifest []byte, store *declaration.Container, modu
"store with. Its server container has to carry the name the foundation raised", "store with. Its server container has to carry the name the foundation raised",
module, store.Name) module, store.Name)
} }
// deliverSuperuser carries the store's superuser password into the module.
//
// It is the foundation's, set on the bundle's store container at genesis; the mesh cannot invent a
// credential that already made the databases, so it goes in through `secret accept`, exactly as the
// control plane's store connections do (control.go deliverStores).
func deliverSuperuser(ctx context.Context, o Options, control controlPlane, module string,
store *declaration.Container, say func(string)) error {
const secret = "superuser"
// Genesis made it and kept it in the file the store was raised from (rootsecrets.go); the
// template's environment variable is accepted too, for a bundle produced before that.
value, err := readCredentialFile(StoreSuperuserFile)
if err != nil {
value = strings.TrimSpace(store.Env["POSTGRES_PASSWORD"])
}
if value == "" {
return fmt.Errorf(
"neither %s nor the foundation's store names the superuser password, so the %s module "+
"has nothing to open the store with — and the mesh cannot invent the one that already "+
"made the databases", StoreSuperuserFile, module)
}
at := "/accepting-" + secret
if err := control.carrying(ctx, "mesh-accepting-"+secret, []byte(value), at); err != nil {
return err
}
if _, err := control.tell(ctx, "secret", "accept", o.Node, module, secret, "--from", at); err != nil {
return err
}
say(" accepted " + secret + " — the store's superuser, as the foundation made it")
return nil
}
+1 -1
View File
@@ -270,7 +270,7 @@ func packagePasswords() (db, admin, builder string, err error) {
func deliverBuilderNpm(ctx context.Context, o Options, control controlPlane, password string, func deliverBuilderNpm(ctx context.Context, o Options, control controlPlane, password string,
say func(string)) error { say func(string)) error {
at := "/accepting-npm-password" at := "/accepting-npm-password"
if err := control.carrying(ctx, "mesh-accepting-npm-password", []byte(password), at); err != nil { if err := control.carryingSecret(ctx, "mesh-accepting-npm-password", []byte(password), at); err != nil {
return err return err
} }
if _, err := control.tell(ctx, "secret", "accept", o.Node, BuilderModule, "npm-password", "--from", at); err != nil { if _, err := control.tell(ctx, "secret", "accept", o.Node, BuilderModule, "npm-password", "--from", at); err != nil {
+5
View File
@@ -332,5 +332,10 @@ func writeBundleFile(path string, content []byte) error {
"applying something nobody can read afterwards is how a machine becomes a mystery", "applying something nobody can read afterwards is how a machine becomes a mystery",
path, err) path, err)
} }
// The mode above applies only when the file is created. A bundle an earlier installer left at
// 0644 would keep that while now carrying real credentials, with this function saying 0600.
if err := os.Chmod(path, 0o600); err != nil {
return fmt.Errorf("cannot make the produced bundle %s readable by root alone: %w", path, err)
}
return nil return nil
} }
+66 -10
View File
@@ -2,8 +2,11 @@ package bootstrap
import ( import (
"bytes" "bytes"
"context"
"crypto/rand" "crypto/rand"
"crypto/sha256"
"encoding/base64" "encoding/base64"
"encoding/hex"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
@@ -80,12 +83,8 @@ func RootSecrets(dryRun bool) (RootCredentials, error) {
} }
func keptOrMade(path string, dryRun bool) (value string, made bool, err error) { func keptOrMade(path string, dryRun bool) (value string, made bool, err error) {
raw, err := os.ReadFile(path) value, err = readCredentialFile(path)
if err == nil { if err == nil {
value = strings.TrimRight(string(raw), "\r\n")
if value == "" {
return "", false, fmt.Errorf("%s exists and is empty; move it aside to have one made", path)
}
return value, false, nil return value, false, nil
} }
if !os.IsNotExist(err) { if !os.IsNotExist(err) {
@@ -113,6 +112,27 @@ func keptOrMade(path string, dryRun bool) (value string, made bool, err error) {
return value, true, nil return value, true, nil
} }
// readCredentialFile is a credential as genesis keeps it: the value alone, its line ending gone.
// Missing is reported as os.IsNotExist so a caller can tell "not made yet" from "unreadable".
func readCredentialFile(path string) (string, error) {
raw, err := os.ReadFile(path)
if err != nil {
return "", err
}
value := strings.TrimRight(string(raw), "\r\n")
if value == "" {
return "", fmt.Errorf("%s exists and is empty; move it aside to have one made", path)
}
return value, nil
}
// credentialFingerprint names a credential without being one — what the broker-admin action
// leaves on the broker's volume, so its verify holds for this value and not for any value.
func credentialFingerprint(value string) string {
sum := sha256.Sum256([]byte(value))
return hex.EncodeToString(sum[:8])
}
// freshSecret is the same shape the controller mints: 30 random bytes as unpadded base64url, 40 // freshSecret is the same shape the controller mints: 30 random bytes as unpadded base64url, 40
// characters, URL-safe — it lands inside connection strings. // characters, URL-safe — it lands inside connection strings.
func freshSecret() (string, error) { func freshSecret() (string, error) {
@@ -123,6 +143,40 @@ func freshSecret() (string, error) {
return base64.RawURLEncoding.EncodeToString(b), nil return base64.RawURLEncoding.EncodeToString(b), nil
} }
// RefuseExistingServers stops a run that would put a made credential in front of a server raised
// by an earlier installer with the template's.
//
// The store's password is set by initdb, once, on an empty volume; the broker's by the action
// above, once. A machine that already holds `mesh-store-data` or `mesh-broker-data` and has no
// credential file was raised with `bootstrap` and `guest`, and minting new values here would make a
// bundle that dials with passwords the servers do not have — failing three steps later, in the
// schemas, with nothing pointing back here. Refused by name instead, with the way forward.
func RefuseExistingServers(ctx context.Context, run Runner, c RootCredentials) error {
for _, check := range []struct {
made bool
volume string
what string
file string
}{
{c.StoreMade, "mesh-store-data", "store", StoreSuperuserFile},
{c.BrokerMade, "mesh-broker-data", "broker", BrokerAdminFile},
} {
if !check.made {
continue
}
if _, err := run(ctx, "docker", "volume", "inspect", check.volume); err != nil {
continue // no such volume: a fresh machine, which is the case this installer makes
}
return fmt.Errorf(
"this machine already holds the %s's data (volume %s) and no credential at %s, so it was raised "+
"by an earlier installer with the template's password. A new one made here would not open it. "+
"Put the password the %s has into %s (0600, the value alone) and run again; then change it "+
"on the server and accept the new value — this installer does not rotate a running %s",
check.what, check.volume, check.file, check.what, check.file, check.what)
}
return nil
}
// RootRewrite says what RewriteRoot did to the bundle. // RootRewrite says what RewriteRoot did to the bundle.
type RootRewrite struct { type RootRewrite struct {
StoreURLs, BrokerURLs int StoreURLs, BrokerURLs int
@@ -162,16 +216,18 @@ func RewriteRoot(r *Rewritten, c RootCredentials) (RootRewrite, error) {
bundle = bytes.ReplaceAll(bundle, []byte(templateBrokerURL), []byte(BrokerAdminUser+":"+c.Broker+"@")) bundle = bytes.ReplaceAll(bundle, []byte(templateBrokerURL), []byte(BrokerAdminUser+":"+c.Broker+"@"))
// The broker's administrator, changed once the broker answers and before anything dials it. // The broker's administrator, changed once the broker answers and before anything dials it.
// Verified by a marker on the broker's own data volume, because the image carries nothing that // Verified by a marker on the broker's own data volume holding this password's fingerprint —
// can try a password from inside; what proves the password is the control plane answering // the image carries nothing that can try a password from inside, and a marker that merely
// over it, a few resources later. // existed would let a regenerated password go unapplied for ever. What proves the password
// works is the control plane answering over it, a few resources later.
fp := credentialFingerprint(c.Broker)
action := templateBrokerReady + "\n" + action := templateBrokerReady + "\n" +
" {\n" + " {\n" +
" \"id\": \"broker-admin\",\n" + " \"id\": \"broker-admin\",\n" +
" \"type\": \"action\",\n" + " \"type\": \"action\",\n" +
" \"in\": \"mesh-broker\",\n" + " \"in\": \"mesh-broker\",\n" +
" \"command\": [\"sh\", \"-c\", \"lavinmqctl change_password " + BrokerAdminUser + " '" + c.Broker + "' && touch " + brokerAdminMarker + "\"],\n" + " \"command\": [\"sh\", \"-c\", \"lavinmqctl change_password " + BrokerAdminUser + " '" + c.Broker + "' && printf %s " + fp + " > " + brokerAdminMarker + "\"],\n" +
" \"verify\": [\"sh\", \"-c\", \"test -f " + brokerAdminMarker + "\"]\n" + " \"verify\": [\"sh\", \"-c\", \"read m < " + brokerAdminMarker + " && [ \\\"$m\\\" = " + fp + " ]\"]\n" +
" }," " },"
if bundle, err = replaceOnce(bundle, templateBrokerReady, action, "the broker's readiness check"); err != nil { if bundle, err = replaceOnce(bundle, templateBrokerReady, action, "the broker's readiness check"); err != nil {
return out, err return out, err
+35 -5
View File
@@ -82,11 +82,9 @@ func (c controlPlane) carry(ctx context.Context, local, remote string) error {
// landed unreadable, `secret accept` failed with `permission denied`, and what depended on it // landed unreadable, `secret accept` failed with `permission denied`, and what depended on it
// crash-looped on material it never received. There is no shell in the image to chown it with. // crash-looped on material it never received. There is no shell in the image to chown it with.
// //
// What goes through here is a module manifest and a store connection string. The connection is the // What goes through here is a module manifest — public, the same bytes as in the catalogue. A
// same value the produced bundle already holds in the clear — a foundation names its own bootstrap // value that is secret goes through carryingSecret below. The file on the machine is removed at
// credentials, and at genesis there is nowhere else for them to be — so this widens nothing. The // once, and the copy inside the container goes when the container does.
// file on the machine is removed at once, and the copy inside the container goes when the
// container does, which for the temporary control plane is step 10.
func (c controlPlane) carrying(ctx context.Context, name string, content []byte, remote string) error { func (c controlPlane) carrying(ctx context.Context, name string, content []byte, remote string) error {
local := filepath.Join(os.TempDir(), name) local := filepath.Join(os.TempDir(), name)
if err := os.WriteFile(local, content, 0o644); err != nil { if err := os.WriteFile(local, content, 0o644); err != nil {
@@ -96,6 +94,38 @@ func (c controlPlane) carrying(ctx context.Context, name string, content []byte,
return c.carry(ctx, local, remote) return c.carry(ctx, local, remote)
} }
// controlPlaneUID is the account the control plane's image runs as — `USER 65534:65534` in its
// Dockerfile — and so the only account inside the container that needs to read what is carried in.
const controlPlaneUID = 65534
// carryingSecret is carrying for a value that is a secret: staged in a directory only root can
// enter, at 0600, owned by the control plane's own account — which `docker cp` keeps, so inside
// the container the file is readable by the process that must read it and by nobody else. Since
// genesis makes the mesh's root credentials rather than copying the template's (rootsecrets.go),
// a store connection string or a broker password carried this way is a real secret, and 0644 in a
// shared temporary directory would hand it to any local user for the length of the copy.
func (c controlPlane) carryingSecret(ctx context.Context, name string, content []byte, remote string) error {
dir, err := os.MkdirTemp("", "mesh-carrying-")
if err != nil {
return fmt.Errorf("nowhere to stage %s before copying it into %s: %w", name, c.container, err)
}
defer os.RemoveAll(dir)
local := filepath.Join(dir, name)
if err := os.WriteFile(local, content, 0o600); err != nil {
return fmt.Errorf("nowhere to stage %s before copying it into %s: %w", name, c.container, err)
}
if err := os.Chown(local, controlPlaneUID, controlPlaneUID); err != nil {
// Not root — a test, or an installer run as a user, which no real genesis is. The
// directory is 0700, so nobody else on the machine can reach the file either way; inside
// the container the only account is the control plane's, so 0644 there is read by it and
// by nothing else. The narrower ownership is taken whenever it can be.
if err := os.Chmod(local, 0o644); err != nil {
return err
}
}
return c.carry(ctx, local, remote)
}
func indent(s string) string { func indent(s string) string {
if s == "" { if s == "" {
return "" return ""