From de809d7e0a941f5c785910507431bd2c987f7521 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 22 Oct 2026 21:36:38 +0200 Subject: [PATCH 1/2] Keep the store's superuser file in the postgres module's own directory, not in /var/lib/postgres (issue 1873) The postgres module's own secrets move from /var/lib/postgres, the postgresql package's directory, to its directory under /var/lib/mesh (mesh-catalog, fix/1873-system-state-dirs). Genesis writes the store's superuser file at the module's own-secret path, so the store it raises is the one the module adopts in place: StoreSuperuserFile follows, to /var/lib/mesh/postgres/superuser.secret. A directory genesis makes on the way is 0755, the mode of the mesh's directories, and only the credential's own is 0700: MkdirAll at 0700 would have closed /var/lib/mesh on a machine that had none yet. --- internal/bootstrap/rootsecrets.go | 34 ++++++++++++++++++++++--- internal/bootstrap/rootsecrets_test.go | 24 +++++++++++++++++ internal/bootstrap/store_layout_test.go | 6 ++--- 3 files changed, 58 insertions(+), 6 deletions(-) diff --git a/internal/bootstrap/rootsecrets.go b/internal/bootstrap/rootsecrets.go index 30ddd11..bae9488 100644 --- a/internal/bootstrap/rootsecrets.go +++ b/internal/bootstrap/rootsecrets.go @@ -45,8 +45,10 @@ import ( // onward, and the permanent controller is given it by `secret accept` (control.go deliverStores). const ( // StoreSuperuserFile is where the store's superuser password lives on the machine — the - // postgres module's own-secret path, so genesis and adoption write the same file. - StoreSuperuserFile = "/var/lib/postgres/superuser.secret" + // postgres module's own-secret path, so genesis and adoption write the same file: in the module's + // directory under /mesh, not in /var/lib/postgres, the postgresql package's own (novox/hq + // issue 1873). + StoreSuperuserFile = "/var/lib/mesh/postgres/superuser.secret" // StoreDataDir is where the store keeps its data — the postgres module's data directory, so // genesis raises the store where the module adopts it (novox/hq ADR 0338 §1). StoreDataDir = "/var/lib/mesh-store" @@ -207,7 +209,7 @@ func keptOrMade(path string, dryRun bool) (value string, made bool, err error) { // writeCredentialFile writes a credential as genesis keeps it. func writeCredentialFile(path, value string) error { - if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { + if err := makeCredentialDir(filepath.Dir(path)); err != nil { return err } // Written whole and renamed into place, at 0600, owned by whoever runs the installer — root, @@ -227,6 +229,32 @@ func writeCredentialFile(path, value string) error { return os.Rename(tmp, path) } +// makeCredentialDir makes a credential's directory 0700 and every missing one above it 0755, the mode the mesh's +// directories have (/var/lib/mesh above a module's own: novox/hq issue 1873), so a module's account still reaches its +// own directory below one genesis made. A directory already there is left as it is. +func makeCredentialDir(dir string) error { + dir = filepath.Clean(dir) + if _, err := os.Stat(dir); err == nil { + return nil + } else if !os.IsNotExist(err) { + return err + } + if parent := filepath.Dir(dir); parent != dir { + if _, err := os.Stat(parent); os.IsNotExist(err) { + if err := makeCredentialDir(parent); err != nil { + return err + } + if err := os.Chmod(parent, 0o755); err != nil { + return err + } + } + } + if err := os.Mkdir(dir, 0o700); err != nil && !os.IsExist(err) { + return err + } + return os.Chmod(dir, 0o700) +} + // readCredentialFile is a credential as genesis keeps it: the value alone, its line ending gone. // Missing is reported as os.IsNotExist so a caller can tell "not made yet" from "unreadable". func readCredentialFile(path string) (string, error) { diff --git a/internal/bootstrap/rootsecrets_test.go b/internal/bootstrap/rootsecrets_test.go index 8cc764e..fa93ae2 100644 --- a/internal/bootstrap/rootsecrets_test.go +++ b/internal/bootstrap/rootsecrets_test.go @@ -249,3 +249,27 @@ func TestTheFoundationFiltersBeforeAnythingListens(t *testing.T) { } } } + +// novox/hq issue 1873: the store's superuser file is the postgres module's own secret in its directory under +// /mesh (/var/lib/mesh/postgres), no longer in /var/lib/postgres, the postgresql package's own. A +// directory genesis makes on the way keeps the mode the mesh's directories have (0755, so every module's account +// reaches its own below it); only the credential's own directory is closed, 0700. +func TestTheStoreSuperuserFileLiesInTheModulesOwnDirectory(t *testing.T) { + if StoreSuperuserFile != "/var/lib/mesh/postgres/superuser.secret" { + t.Errorf("the store's superuser file is %s, not the postgres module's /var/lib/mesh/postgres/superuser.secret", StoreSuperuserFile) + } + dir := t.TempDir() + store := dir + "/mesh/postgres/superuser.secret" + if _, err := rootSecretsAt(store, dir+"/genesis/bus", dir+"/genesis/bus.bcrypt", false); err != nil { + t.Fatal(err) + } + for path, want := range map[string]os.FileMode{dir + "/mesh": 0o755, dir + "/mesh/postgres": 0o700} { + st, err := os.Stat(path) + if err != nil { + t.Fatal(err) + } + if st.Mode().Perm() != want { + t.Errorf("%s is made %04o, not %04o", path, st.Mode().Perm(), want) + } + } +} diff --git a/internal/bootstrap/store_layout_test.go b/internal/bootstrap/store_layout_test.go index 1fe597f..1f36b4d 100644 --- a/internal/bootstrap/store_layout_test.go +++ b/internal/bootstrap/store_layout_test.go @@ -66,11 +66,11 @@ func TestTheBundleRaisesTheStoreAsTheCataloguesPostgresModuleDeclaresIt(t *testi if _, has := ours.Env["POSTGRES_PASSWORD"]; has { t.Error("the bundle's store takes its password from its environment; the module reads a file") } - // The module's own directory for its state is /, where genesis keeps the - // superuser's file (StoreSuperuserFile). + // The module's own directory is /mesh/, where genesis keeps the superuser's + // file (StoreSuperuserFile; novox/hq issue 1873). var theirs []string for _, v := range r.Volumes { - theirs = append(theirs, strings.ReplaceAll(v, "${dir:state}", "/var/lib/postgres")) + theirs = append(theirs, strings.ReplaceAll(v, "${dir:data}", "/var/lib/mesh/postgres")) } if strings.Join(ours.Volumes, " ") != strings.Join(theirs, " ") { t.Errorf("the bundle's store mounts %v; the module's %v", ours.Volumes, theirs) -- 2.54.0 From 8c0bb9d4531d45121b7d0dd37bdbc4441c8d4f40 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 22 Oct 2026 21:46:34 +0200 Subject: [PATCH 2/2] Read the bus's release from the nats module's pinned server image, not a NATS_BASE build argument (issue 1875) The nats module takes its server as an upstream artifact, pinned by digest (issue 1641), and builds no image on a NATS_BASE argument any more. The test of the bundle's bus read that argument, found none, and failed every mesh-host check against the catalogue's main. Bootstrap's own code never read it: the bundle's bus image comes from the bundle, so genesis was not affected. The test now compares the bundle's image with the module's server by repository and digest. --- internal/bootstrap/bus_adoption_test.go | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/internal/bootstrap/bus_adoption_test.go b/internal/bootstrap/bus_adoption_test.go index cbb52e9..1c16bf1 100644 --- a/internal/bootstrap/bus_adoption_test.go +++ b/internal/bootstrap/bus_adoption_test.go @@ -108,28 +108,35 @@ func TestABusStepThatEndsFailedIsRefused(t *testing.T) { } } -// The bundle's bus runs the release the module's image is built on, pinned by the same digest, so the -// adoption changes no server release and is reversible as the step says (ADR 0333 §3). +// The bundle's bus runs the release the module's server is, pinned by the same digest, so the adoption changes no +// server release and is reversible as the step says (ADR 0333 §3). The module takes the server as an upstream +// artifact, its image reference pinned by digest (issue 1641); it no longer builds an image on a NATS_BASE argument, +// which this test read until issue 1875. func TestTheBundlesBusRunsTheReleaseTheModulesImageIsBuiltOn(t *testing.T) { var m struct { Build struct { - On []struct{ Arg, Image string } `json:"on"` + Artifacts []struct{ Name, Kind, From string } `json:"artifacts"` } `json:"build"` } if err := json.Unmarshal(theCataloguesModule(t, BusModule), &m); err != nil { t.Fatal(err) } - base := "" - for _, on := range m.Build.On { - if on.Arg == "NATS_BASE" { - base = on.Image + from := "" + for _, a := range m.Build.Artifacts { + if a.Name == "server" && a.Kind == "upstream" { + from = a.From } } + name, digest, pinned := strings.Cut(from, "@") + if !pinned || !strings.HasPrefix(digest, "sha256:") { + t.Fatalf("the nats module's server is no upstream image pinned by digest: %q", from) + } + repository, _, _ := strings.Cut(name, ":") rewritten, err := Rewrite(theRealBundle(t), held) if err != nil { t.Fatal(err) } - if got := containerNamed(rewritten.Declaration, "mesh-broker-nats").Image; got != base { - t.Errorf("the bundle raises the bus on %s; the nats module's image is built on %s", got, base) + if got, want := containerNamed(rewritten.Declaration, "mesh-broker-nats").Image, repository+"@"+digest; got != want { + t.Errorf("the bundle raises the bus on %s; the nats module's server is %s (%s)", got, want, from) } } -- 2.54.0