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.
124 lines
4.2 KiB
Go
124 lines
4.2 KiB
Go
package bootstrap
|
|
|
|
import (
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-host/internal/declaration"
|
|
)
|
|
|
|
// The produced bundle carries no well-known credential: the store reads its password from the
|
|
// file genesis made, every connection string names the made values, and the broker's default
|
|
// administrator is changed by an action before anything dials it (novox/hq issue 071).
|
|
func TestTheProducedBundleCarriesNoWellKnownCredential(t *testing.T) {
|
|
template, err := os.ReadFile("../../examples/foundation-first-node.lock")
|
|
if err != nil {
|
|
t.Skip("no example bundle beside this checkout")
|
|
}
|
|
r, err := Rewrite(template, "sha256:"+strings.Repeat("ab", 32))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
creds := RootCredentials{Store: "STORE-PW-40-characters-of-random-base64u", Broker: "BROKER-PW-40-characters-of-random-base64"}
|
|
got, err := RewriteRoot(&r, creds)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
text := string(r.Bundle)
|
|
for _, gone := range []string{`"bootstrap"`, "postgres:bootstrap@", "guest:guest@"} {
|
|
if strings.Contains(text, gone) {
|
|
t.Errorf("the produced bundle still says %s", gone)
|
|
}
|
|
}
|
|
if got.StoreURLs < 3 || got.BrokerURLs < 2 {
|
|
t.Errorf("rewrote %d store and %d broker connections; the template has three and two", got.StoreURLs, got.BrokerURLs)
|
|
}
|
|
var store, action bool
|
|
for _, res := range r.Declaration.Resources {
|
|
switch x := res.(type) {
|
|
case *declaration.Container:
|
|
if x.Name != "mesh-store" {
|
|
continue
|
|
}
|
|
store = true
|
|
if _, has := x.Env["POSTGRES_PASSWORD"]; has {
|
|
t.Error("the store still takes its password from its environment")
|
|
}
|
|
if x.Env["POSTGRES_PASSWORD_FILE"] != storeSuperuserMount {
|
|
t.Errorf("the store reads its password from %q", x.Env["POSTGRES_PASSWORD_FILE"])
|
|
}
|
|
if !strings.Contains(strings.Join(x.Volumes, " "), StoreSuperuserFile+":"+storeSuperuserMount) {
|
|
t.Errorf("the store does not mount %s: %v", StoreSuperuserFile, x.Volumes)
|
|
}
|
|
case *declaration.Action:
|
|
if x.ID != "broker-admin" {
|
|
continue
|
|
}
|
|
action = true
|
|
if x.In != "mesh-broker" || !strings.Contains(strings.Join(x.Command, " "), "change_password "+BrokerAdminUser+" '"+creds.Broker+"'") {
|
|
t.Errorf("the broker-admin action is %v in %q", x.Command, x.In)
|
|
}
|
|
}
|
|
}
|
|
if !store || !action {
|
|
t.Fatalf("store=%v action=%v", store, action)
|
|
}
|
|
// The order matters: the broker's password changes after it answers and before the control
|
|
// plane, which dials it with the new one, is raised.
|
|
var readyAt, adminAt, controlAt int
|
|
for i, res := range r.Declaration.Resources {
|
|
switch res.Identity() {
|
|
case "broker-ready":
|
|
readyAt = i
|
|
case "broker-admin":
|
|
adminAt = i
|
|
case "control-plane":
|
|
controlAt = i
|
|
}
|
|
}
|
|
if !(readyAt < adminAt && adminAt < controlAt) {
|
|
t.Errorf("order ready=%d admin=%d control=%d", readyAt, adminAt, controlAt)
|
|
}
|
|
}
|
|
|
|
// A template that no longer says what this expects is refused, not half-rewritten.
|
|
func TestATemplateWithoutTheKnownCredentialsIsRefused(t *testing.T) {
|
|
template, err := os.ReadFile("../../examples/foundation-first-node.lock")
|
|
if err != nil {
|
|
t.Skip("no example bundle beside this checkout")
|
|
}
|
|
changed := strings.Replace(string(template), `"POSTGRES_PASSWORD": "bootstrap"`, `"POSTGRES_PASSWORD": "other"`, 1)
|
|
r, err := Rewrite([]byte(changed), "sha256:"+strings.Repeat("ab", 32))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := RewriteRoot(&r, RootCredentials{Store: "x", Broker: "y"}); err == nil {
|
|
t.Fatal("a template with an unknown store password was rewritten")
|
|
}
|
|
}
|
|
|
|
// Made once and kept: a second run reads the same value; a dry run writes nothing.
|
|
func TestRootSecretsAreKeptAcrossRuns(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := dir + "/superuser.secret"
|
|
first, made, err := keptOrMade(path, false)
|
|
if err != nil || !made || len(first) != 40 {
|
|
t.Fatalf("first: %q made=%v err=%v", first, made, err)
|
|
}
|
|
if info, _ := os.Stat(path); info.Mode().Perm() != 0o600 {
|
|
t.Errorf("mode %v", info.Mode().Perm())
|
|
}
|
|
second, made, err := keptOrMade(path, false)
|
|
if err != nil || made || second != first {
|
|
t.Fatalf("second: %q made=%v err=%v", second, made, err)
|
|
}
|
|
dry := dir + "/dry.secret"
|
|
if _, made, err := keptOrMade(dry, true); err != nil || !made {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := os.Stat(dry); err == nil {
|
|
t.Fatal("a dry run wrote a secret")
|
|
}
|
|
}
|