031: a window of unacknowledged declarations is drained to the newest; the rest are set aside and reported as superseded. 035: a file resource may say create-once — written when absent, kept untouched when present (ADR 0087). 054: the bundle installs nftables and loads a base ruleset before the store and broker, in the table the filter module later replaces (ADR 0088).
214 lines
7.7 KiB
Go
214 lines
7.7 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")
|
|
}
|
|
}
|
|
|
|
// 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", Broker: "BROKER-PW"})
|
|
say("created context-schemas (docker run -e MESH_STORE_INVENTORY=postgres://postgres:STORE-PW@127.0.0.1:5432/inventory)")
|
|
say("failed broker-admin (sh -c lavinmqctl change_password guest 'BROKER-PW' && echo x)")
|
|
for _, l := range said {
|
|
if strings.Contains(l, "STORE-PW") || strings.Contains(l, "BROKER-PW") {
|
|
t.Errorf("said a credential: %s", l)
|
|
}
|
|
}
|
|
if !strings.Contains(said[0], "postgres:…@") || !strings.Contains(said[1], "guest '…'") {
|
|
t.Errorf("the lines were not the same lines with the values masked: %v", said)
|
|
}
|
|
}
|
|
|
|
// The broker-admin marker is written with a line ending, because the verify reads it with `read`.
|
|
func TestTheBrokerAdminMarkerHasALineEnding(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)
|
|
}
|
|
if _, err := RewriteRoot(&r, RootCredentials{Store: "s", Broker: "b"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, res := range r.Declaration.Resources {
|
|
a, ok := res.(*declaration.Action)
|
|
if !ok || a.ID != "broker-admin" {
|
|
continue
|
|
}
|
|
cmd := strings.Join(a.Command, " ")
|
|
if !strings.Contains(cmd, "&& echo ") || strings.Contains(cmd, "printf %s") {
|
|
t.Errorf("the marker is written without a line ending: %s", cmd)
|
|
}
|
|
if !strings.Contains(strings.Join(a.Verify, " "), "read m <") {
|
|
t.Errorf("the verify does not read the marker: %v", a.Verify)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
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)
|
|
}
|
|
if _, err := RewriteRoot(&r, RootCredentials{Store: "s", Broker: "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", "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)
|
|
}
|
|
}
|
|
}
|