package bootstrap import ( "os" "strings" "testing" "golang.org/x/crypto/bcrypt" "github.com/novox/mesh-host/internal/declaration" ) // testCredentials are made values with a real bcrypt hash of the bus password, as RootSecrets makes. func testCredentials(t *testing.T, store, bus string) RootCredentials { t.Helper() hash, err := bcrypt.GenerateFromPassword([]byte(bus), bcrypt.MinCost) if err != nil { t.Fatal(err) } return RootCredentials{Store: store, Bus: bus, BusHash: string(hash)} } // theExample is the one genesis bundle, rewritten for an image as the installer does. func theExample(t *testing.T) Rewritten { t.Helper() 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) } return r } // The produced bundle carries no well-known credential: the store reads its password from the // file genesis made, and the controller's account on the bus is the made one in both places the // bundle names it — the address the control plane dials and the user list the bus is raised with // (novox/hq issue 071, ADR 0331 §3). func TestTheProducedBundleCarriesNoWellKnownCredential(t *testing.T) { r := theExample(t) creds := testCredentials(t, "STORE-PW-40-characters-of-random-base64u", "BUS-PW-40-characters-of-random-base64uuu") got, err := RewriteRoot(&r, creds) if err != nil { t.Fatal(err) } text := string(r.Bundle) for _, gone := range []string{`"bootstrap"`, "postgres:bootstrap@", "controller:bootstrap@"} { if strings.Contains(text, gone) { t.Errorf("the produced bundle still says %s", gone) } } if got.StoreURLs < 3 || got.BusURLs != 1 { t.Errorf("rewrote %d store and %d bus connections; the template has three and one", got.StoreURLs, got.BusURLs) } var store, accounts, control bool for _, res := range r.Declaration.Resources { switch x := res.(type) { case *declaration.Container: switch x.Name { case "mesh-store": 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 r.TempName: control = true if want := "nats://controller:" + creds.Bus + "@127.0.0.1:5671"; x.Env[BusVar] != want { t.Errorf("the temporary control plane dials the bus at %q, not with the made account", x.Env[BusVar]) } } case *declaration.File: if x.ID != "bus-accounts" { continue } accounts = true hash := hashOf(t, x.Content, "controller") if bcrypt.CompareHashAndPassword([]byte(hash), []byte(creds.Bus)) != nil { t.Errorf("the bus is raised with a controller account whose hash %q is not the made password's", hash) } if bcrypt.CompareHashAndPassword([]byte(hash), []byte("bootstrap")) == nil { t.Error("the bus is still raised with the controller at bootstrap") } } } if !store || !accounts || !control { t.Fatalf("store=%v accounts=%v control=%v", store, accounts, control) } } // hashOf is the password hash a NATS user list gives one user. func hashOf(t *testing.T, list, user string) string { t.Helper() marker := `{ user: "` + user + `", password: "` at := strings.Index(list, marker) if at < 0 { t.Fatalf("the user list names no %s:\n%s", user, list) } rest := list[at+len(marker):] return rest[:strings.Index(rest, `"`)] } // The bus's placeholders are what the installer looks for, and a template naming no bus connection // it knows is refused at preflight, naming the placeholder (novox/hq ADR 0331 §3, §4). func TestABundleNamingNoBusConnectionItKnowsIsRefusedByItsPlaceholder(t *testing.T) { template, err := os.ReadFile("../../examples/foundation-first-node.lock") if err != nil { t.Skip("no example bundle beside this checkout") } parse := func(b string) *declaration.Declaration { d, err := declaration.ParseFileTrusted([]byte(b)) if err != nil { t.Fatal(err) } return d } if err := BusPlaceholders(template, parse(string(template))); err != nil { t.Fatalf("the shipped bundle is refused: %v", err) } for _, c := range []struct{ what, from, to, names string }{ {"no bus address", `"MESH_BUS_NATS": "nats://controller:bootstrap@127.0.0.1:5671",`, "", BusVar}, {"the previous broker's", `"MESH_BUS_NATS": "nats://controller:bootstrap@127.0.0.1:5671"`, `"MESH_BUS_NATS": "amqp://guest:guest@127.0.0.1:5671"`, BusURLPlaceholder}, {"no pinned certificate", `"MESH_BROKER_CERTIFICATE": "/broker-tls/tls.crt"`, `"MESH_OTHER": "x"`, CertificateVar}, {"no controller in the user list", `{ user: \"controller\"`, `{ user: \"someone\"`, BusUserPlaceholder}, } { if !strings.Contains(string(template), c.from) { t.Fatalf("%s: the bundle does not say %s", c.what, c.from) } changed := strings.Replace(string(template), c.from, c.to, 1) err := BusPlaceholders([]byte(changed), parse(changed)) if err == nil { t.Errorf("%s: accepted", c.what) continue } if !strings.Contains(err.Error(), c.names) { t.Errorf("%s: refused without naming %s: %v", c.what, c.names, err) } if strings.Contains(err.Error(), "guest:guest") { t.Errorf("%s: the refusal quotes a password: %v", c.what, err) } } } // The account is made once, with its hash, and a second run puts the same bytes in the bundle — a // hash salted afresh every run would rewrite the bus's user list every run. func TestTheBusAccountIsKeptWithItsHash(t *testing.T) { dir := t.TempDir() first, err := rootSecretsAt(dir+"/store", dir+"/bus", dir+"/bus.bcrypt", false) if err != nil { t.Fatal(err) } if !first.BusMade || len(first.Bus) != 40 || bcrypt.CompareHashAndPassword([]byte(first.BusHash), []byte(first.Bus)) != nil { t.Fatalf("first run: made=%v hash ok=%v", first.BusMade, bcrypt.CompareHashAndPassword([]byte(first.BusHash), []byte(first.Bus)) == nil) } second, err := rootSecretsAt(dir+"/store", dir+"/bus", dir+"/bus.bcrypt", false) if err != nil { t.Fatal(err) } if second.BusMade || second.Bus != first.Bus || second.BusHash != first.BusHash { t.Fatalf("a second run changed the bus account: made=%v same=%v same hash=%v", second.BusMade, second.Bus == first.Bus, second.BusHash == first.BusHash) } } // 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") } } // Nothing the installer says after making the credentials contains them. func TestTheTranscriptNeverSaysTheCredentials(t *testing.T) { var said []string say := Masking(func(l string) { said = append(said, l) }, RootCredentials{Store: "STORE-PW", Bus: "BUS-PW"}) say("created context-schemas (docker run -e MESH_STORE_INVENTORY=postgres://postgres:STORE-PW@127.0.0.1:5432/inventory)") say("failed control-plane (MESH_BUS_NATS=nats://controller:BUS-PW@127.0.0.1:5671)") for _, l := range said { if strings.Contains(l, "STORE-PW") || strings.Contains(l, "BUS-PW") { t.Errorf("said a credential: %s", l) } } if !strings.Contains(said[0], "postgres:…@") || !strings.Contains(said[1], "controller:…@") { t.Errorf("the lines were not the same lines with the values masked: %v", said) } } // The foundation filters before anything listens: nftables is in place before the store, and its // rules refuse the store's and broker's client ports from outside while keeping ssh, the bus a node // enrols over and the registry a node pulls from (novox/hq issue 054). func TestTheFoundationFiltersBeforeAnythingListens(t *testing.T) { r := theExample(t) if _, err := RewriteRoot(&r, testCredentials(t, "s", "b")); err != nil { t.Fatal(err) } var filterAt, loadedAt, storeAt, brokerAt = -1, -1, -1, -1 var rules string for i, res := range r.Declaration.Resources { switch res.Identity() { case "base-filter": filterAt = i rules = res.(*declaration.File).Content case "base-filter-loaded": loadedAt = i case "store": storeAt = i case "broker": brokerAt = i } } if !(filterAt >= 0 && filterAt < loadedAt && loadedAt < storeAt && storeAt < brokerAt) { t.Fatalf("order: filter %d loaded %d store %d broker %d", filterAt, loadedAt, storeAt, brokerAt) } for _, want := range []string{"policy drop", "tcp dport 22 accept", "ct original proto-dst 5671 accept", "ct original proto-dst 5000 accept", "tcp dport 5671 accept", "tcp dport 5000 accept", "ip saddr 172.16.0.0/12 accept", "table inet mesh"} { if !strings.Contains(rules, want) { t.Errorf("the base filter lacks %q", want) } } for _, mustNot := range []string{"5432", "5672"} { if strings.Contains(rules, mustNot) { t.Errorf("the base filter names %s, which must be reached from the machine and its containers only", mustNot) } } }