mesh/merge-gate head 52600ec7bd61 merged into trunk cdcba9ca7375 — pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes wi…
mesh/repo-check head 52600ec7bd61 merged into trunk cdcba9ca7375 — pass: its merge-check.sh passed
mesh/delivery delivered
252 lines
9.5 KiB
Go
252 lines
9.5 KiB
Go
package bootstrap
|
|
|
|
import (
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/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 "postgres":
|
|
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)
|
|
}
|
|
}
|
|
}
|