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.
150 lines
6.8 KiB
Go
150 lines
6.8 KiB
Go
package bootstrap
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// controlPlane is the running control plane, asked things.
|
|
//
|
|
// **Through `docker exec`, not over a network.** The control plane listens on nothing — `serve` is
|
|
// a broker consumer, and every administrative verb is a subcommand of the same binary that opens
|
|
// the stores directly (mesh-controller's own usage). So the way to tell a mesh anything, from the
|
|
// machine the mesh is on, is to run its binary inside its own container. That is also what the lab
|
|
// does, and having the installer and the lab drive the mesh identically is the point: the lab is
|
|
// meant to exercise the installer, not a second procedure that resembles it.
|
|
//
|
|
// It carries which container, because the whole pivot turns on there being two of them: the
|
|
// foundation's `temp-mesh-controller` for steps 6 to 9, and the module's `mesh-controller` afterwards.
|
|
type controlPlane struct {
|
|
container string
|
|
run Runner
|
|
timeout time.Duration
|
|
}
|
|
|
|
// within is the same control plane, asked with a different patience.
|
|
//
|
|
// A copy rather than a field somebody sets, so a slow command cannot leave every command after it
|
|
// slow: the caller that needs the long wait says so on the call.
|
|
func (c controlPlane) within(timeout time.Duration) controlPlane {
|
|
c.timeout = timeout
|
|
return c
|
|
}
|
|
|
|
// tell runs a mesh-controller subcommand and gives back what it said.
|
|
//
|
|
// The failure carries the command AND the output. A mesh-controller refusal is a paragraph explaining
|
|
// what is wrong — "nothing provides route, wanted by registry" — and an installer that reported
|
|
// only "exit status 1" would throw away the one thing a person needs.
|
|
func (c controlPlane) tell(ctx context.Context, args ...string) (string, error) {
|
|
asking, cancel := context.WithTimeout(ctx, c.timeout)
|
|
defer cancel()
|
|
|
|
out, err := c.run(asking, "docker", append(
|
|
[]string{"exec", c.container, controlPlaneBinary}, args...)...)
|
|
if err != nil {
|
|
return out, fmt.Errorf("`%s %s` was refused: %w\n%s",
|
|
c.container, strings.Join(args, " "), err, indent(strings.TrimSpace(out)))
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// carry puts a file inside the control plane's container.
|
|
//
|
|
// **Because every command that takes a file reads it from its own filesystem.** `module add
|
|
// <file>` and `secret accept --from <file>` open a path, and the process doing the opening is
|
|
// inside the container. The installer is not. So the file is copied in first, exactly as the lab
|
|
// does it.
|
|
//
|
|
// The image is `FROM scratch` and has no shell, so nothing inside can move a file, change its mode
|
|
// or clean up after itself. What is copied in stays until the container is replaced — which, for
|
|
// the temporary control plane, is a container that gets removed at step 10 and takes its contents
|
|
// with it.
|
|
func (c controlPlane) carry(ctx context.Context, local, remote string) error {
|
|
asking, cancel := context.WithTimeout(ctx, c.timeout)
|
|
defer cancel()
|
|
|
|
if _, err := c.run(asking, "docker", "cp", local, c.container+":"+remote); err != nil {
|
|
return fmt.Errorf("cannot put %s into %s at %s: %w", local, c.container, remote, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// carrying writes bytes to a temporary file on the machine and copies them into the container.
|
|
//
|
|
// **0644, and that is not carelessness — 0600 would break it.** `docker cp` keeps the ownership and
|
|
// mode a file had outside, the control plane's image runs as 65534, and the process that reads
|
|
// these files is that one. The lab paid for this exactly once: a 0600 root-owned key copied in
|
|
// 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.
|
|
//
|
|
// What goes through here is a module manifest — public, the same bytes as in the catalogue. A
|
|
// value that is secret goes through carryingSecret below. The file on the machine is removed at
|
|
// once, and the copy inside the container goes when the container does.
|
|
func (c controlPlane) carrying(ctx context.Context, name string, content []byte, remote string) error {
|
|
local := filepath.Join(os.TempDir(), name)
|
|
if err := os.WriteFile(local, content, 0o644); err != nil {
|
|
return fmt.Errorf("nowhere to stage %s before copying it into %s: %w", name, c.container, err)
|
|
}
|
|
defer os.Remove(local)
|
|
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 {
|
|
if s == "" {
|
|
return ""
|
|
}
|
|
return " " + strings.ReplaceAll(s, "\n", "\n ")
|
|
}
|
|
|
|
// mentions reports whether one of a listing's lines starts with this exact word.
|
|
//
|
|
// Line-and-word rather than a substring search, because these listings are columns and a
|
|
// substring match would find `registry` inside `registry-mirror` and report a module installed
|
|
// that is not. Every one of mesh-controller's `list` verbs prints the name first on the line.
|
|
func mentions(listing, name string) bool {
|
|
for _, line := range strings.Split(listing, "\n") {
|
|
first, _, _ := strings.Cut(strings.TrimSpace(line), " ")
|
|
if first == name {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|