From 553e7857df3da61f86df92d32eec71c63b258f6e Mon Sep 17 00:00:00 2001 From: Julian Sommer <115516918+juliansommer@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:27:00 +1000 Subject: [PATCH] feat: enforce nats authentication (#123) --- cmd/spinifex/cmd/service.go | 1 + cmd/spinifex/cmd/templates/nats.conf | 4 +- spinifex/services/nats/nats_test.go | 88 ++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 spinifex/services/nats/nats_test.go diff --git a/cmd/spinifex/cmd/service.go b/cmd/spinifex/cmd/service.go index 26d8a5d34..fd1e106e1 100644 --- a/cmd/spinifex/cmd/service.go +++ b/cmd/spinifex/cmd/service.go @@ -287,6 +287,7 @@ var viperblockStartCmd = &cobra.Command{ service, err := service.New("viperblock", &viperblockd.Config{ NatsHost: nodeConfig.NATS.Host, + NatsToken: nodeConfig.NATS.ACL.Token, PluginPath: pluginPath, S3Host: nodeConfig.Predastore.Host, Bucket: nodeConfig.Predastore.Bucket, diff --git a/cmd/spinifex/cmd/templates/nats.conf b/cmd/spinifex/cmd/templates/nats.conf index 82216c6f8..e2a438ec7 100644 --- a/cmd/spinifex/cmd/templates/nats.conf +++ b/cmd/spinifex/cmd/templates/nats.conf @@ -16,7 +16,7 @@ cluster { routes = [ {{- range .ClusterRoutes }} - "nats://{{ . }}", + "nats-route://{{$.NatsToken}}@{{ . }}", {{- end }} ] } @@ -26,7 +26,7 @@ server_name: spinifex-nats-{{.Node}} # Authorization authorization { -# token: "{{.NatsToken}}" + token: "{{.NatsToken}}" } # JetStream for persistent messaging and KV store diff --git a/spinifex/services/nats/nats_test.go b/spinifex/services/nats/nats_test.go new file mode 100644 index 000000000..9f483155b --- /dev/null +++ b/spinifex/services/nats/nats_test.go @@ -0,0 +1,88 @@ +package nats + +import ( + "os" + "path/filepath" + "runtime" + "testing" + "text/template" + "time" + + "github.com/nats-io/nats-server/v2/server" + "github.com/nats-io/nats.go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// templatePath returns the absolute path to the canonical nats.conf template +// used by admin init. Reading at test time (rather than embedding a copy) +// ensures the test always validates the real template. +func templatePath(t *testing.T) string { + t.Helper() + _, thisFile, _, ok := runtime.Caller(0) + require.True(t, ok) + return filepath.Join(filepath.Dir(thisFile), "..", "..", "..", "cmd", "spinifex", "cmd", "templates", "nats.conf") +} + +// TestRenderedConfig_EnforcesAuth renders the production nats.conf template with +// a known token, starts an embedded NATS server from the resulting config, and +// verifies that unauthenticated and wrong-token connections are rejected while +// correct-token connections succeed. +func TestRenderedConfig_EnforcesAuth(t *testing.T) { + token := "nats_test-secret-token-1234" + tmpDir := t.TempDir() + confPath := filepath.Join(tmpDir, "nats.conf") + + // Read and render the production template. + raw, err := os.ReadFile(templatePath(t)) + require.NoError(t, err) + + tmpl, err := template.New("nats.conf").Parse(string(raw)) + require.NoError(t, err) + + f, err := os.Create(confPath) + require.NoError(t, err) + + err = tmpl.Execute(f, map[string]any{ + "BindIP": "127.0.0.1", + "Node": "test-node", + "NatsToken": token, + "DataDir": tmpDir, + "LogDir": tmpDir, + }) + f.Close() + require.NoError(t, err) + + // Start NATS server from the rendered config. + opts, err := server.ProcessConfigFile(confPath) + require.NoError(t, err) + + // Override for test isolation: random port, no monitoring. + opts.Port = -1 + opts.LogFile = "" + opts.HTTPHost = "" + opts.HTTPPort = 0 + + ns, err := server.NewServer(opts) + require.NoError(t, err) + go ns.Start() + require.True(t, ns.ReadyForConnections(5*time.Second)) + t.Cleanup(func() { ns.Shutdown() }) + + t.Run("no token rejected", func(t *testing.T) { + _, err := nats.Connect(ns.ClientURL(), nats.MaxReconnects(0)) + assert.Error(t, err, "unauthenticated connection should be rejected") + }) + + t.Run("wrong token rejected", func(t *testing.T) { + _, err := nats.Connect(ns.ClientURL(), nats.Token("wrong-token"), nats.MaxReconnects(0)) + assert.Error(t, err, "wrong token should be rejected") + }) + + t.Run("correct token accepted", func(t *testing.T) { + nc, err := nats.Connect(ns.ClientURL(), nats.Token(token), nats.MaxReconnects(0)) + require.NoError(t, err, "correct token should be accepted") + defer nc.Close() + assert.True(t, nc.IsConnected()) + }) +}