From 70341cfbc7816047aad5adf0c5e0a1617eaf2a46 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 28 Sep 2026 01:40:10 +0200 Subject: [PATCH] The bus account has JetStream, and the control plane's client has its own inbox MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two refusals the first live connections met. A user in the MESH account was told "JetStream not enabled for account" the first time it bound a consumer: with accounts defined, JetStream is enabled per account, not only globally — the account's setting, which the mesh owns, not the server's block, which it does not. And the control plane's client used a random inbox prefix where it is granted exactly _INBOX..>, so the server's first answer could not reach it. The prefix now follows from the user in the URL, for every principal that dials so. --- internal/broker/jetstream.go | 6 ++++++ internal/broker/nats.go | 5 ++++- internal/broker/testdata/composed.conf | 1 + internal/broker/users_test.go | 8 +++++++- 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/internal/broker/jetstream.go b/internal/broker/jetstream.go index 5ae5c51..1ee9ceb 100644 --- a/internal/broker/jetstream.go +++ b/internal/broker/jetstream.go @@ -42,6 +42,12 @@ func Dial(url string, opts ...nats.Option) (*JetStream, error) { } opts = append(opts, nats.Secure(pinned)) } + // **Its own inbox, and nothing wider.** Every principal is granted `_INBOX..>` and + // no other inbox; the client's default prefix is random, and the server refused the first + // subscription to it (2026-09-28). The user is in the URL, so the prefix follows from it. + if user, _, _ := CredentialIn(url); user != "" { + opts = append(opts, nats.CustomInboxPrefix("_INBOX."+user)) + } // The address in an error is the address alone. The URL carries this controller's password, // and an error here is written on the assumption it will be logged. where := BareAddress(url) diff --git a/internal/broker/nats.go b/internal/broker/nats.go index 00830c7..562c376 100644 --- a/internal/broker/nats.go +++ b/internal/broker/nats.go @@ -521,7 +521,10 @@ func ComposeAccounts(principals []Principal) (string, error) { // One account for the mesh: accounts in NATS isolate subject spaces entirely, and the mesh is // one space (design 25 §4). The cost of that — that permissions are the only isolation — is // paid in the scoping of every inbox and every ack subject. - b.WriteString("accounts {\n MESH {\n users = [\n") + // JetStream is enabled per account once accounts exist at all: with only the global block set, + // a user in MESH is told "JetStream not enabled for account" the first time it binds a + // consumer, which is the first thing every host does (2026-09-28). + b.WriteString("accounts {\n MESH {\n jetstream: enabled\n users = [\n") for _, p := range sorted { perms, err := PermissionsFor(p) if err != nil { diff --git a/internal/broker/testdata/composed.conf b/internal/broker/testdata/composed.conf index d764044..6e6eb35 100644 --- a/internal/broker/testdata/composed.conf +++ b/internal/broker/testdata/composed.conf @@ -21,6 +21,7 @@ jetstream { accounts { MESH { + jetstream: enabled users = [ { user: "controller", password: "$2a$11$cccccccccccccccccccccc", permissions: { publish: { allow: ["$JS.ACK.CONTROL.controller.>", "$JS.ACK.EVENTS.controller.>", "$JS.API.>", "_INBOX.enrol.>", "mesh.control.>", "mesh.node.>", "mesh.seat.mesh-build-machine.accept.>"] } diff --git a/internal/broker/users_test.go b/internal/broker/users_test.go index 0e21eaf..83e8d86 100644 --- a/internal/broker/users_test.go +++ b/internal/broker/users_test.go @@ -186,7 +186,13 @@ func TestWhatTheMeshWritesIsUsersAndNothingAboutTheServer(t *testing.T) { } // None of the server's own settings. Each of these in the mesh's file is a value the controller // would then own, and the module could no longer change its own image without the mesh agreeing. - for _, absent := range []string{"port:", "http:", "jetstream", "tls {", "store_dir", "cert_file"} { + // `jetstream {` is the server's block (its store, its limits); `jetstream: enabled` inside the + // account is the account's, and the mesh owns the account — a user in it is told "JetStream + // not enabled for account" without it (2026-09-28). + if !strings.Contains(got, "jetstream: enabled") { + t.Errorf("the account does not enable JetStream, so no user in it can bind a consumer") + } + for _, absent := range []string{"port:", "http:", "jetstream {", "tls {", "store_dir", "cert_file"} { if strings.Contains(got, absent) { t.Errorf("the accounts file contains %q, which belongs to the module that raises the "+ "server, not to the mesh", absent)