Files
unibus/pkg/membership/posture_test.go
T
egutierrez 9b96537aa6 fix(0006d): enforce homogeneous cluster posture + publish posture on /healthz (audit 0008 N1)
A cluster is only as secure as its weakest node: the data plane forwards every
subject between nodes, so one node running without enforced auth lets an
unauthenticated peer Subscribe(">") on it and harvest the traffic forwarded from
the ACL'd nodes.

- validateClusterConfig now takes the auth mode and REFUSES to join a cluster
  unless --bus-auth enforce, regardless of bind (a clustered node is a production
  node; there is no safe dev cluster without auth). This binary therefore cannot
  BE the weak node.
- Server.Posture {enforce,acl,tls,cluster,store} is published on /healthz (non
  secret operational metadata, probe stays unauthenticated) so a monitor or peer
  can detect a cluster member not running enforce+ACL+TLS — covering a peer that
  runs a tampered/old binary outside this node's control.

Tests:
- TestAttack0008_N1: a clustered node with --bus-auth off is refused; the same
  node with enforce + full route security is allowed.
- TestClusterConfigPolicy: extended with off/soft clustered cases (refused) and
  the mode parameter throughout.
- TestHealthExposesPosture: /healthz returns the posture booleans + store backend.

CGO_ENABLED=0 go build/vet/test green; govulncheck 0 reachable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 17:17:37 +02:00

58 lines
1.7 KiB
Go

package membership_test
import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"github.com/enmanuel/unibus/pkg/blobstore"
"github.com/enmanuel/unibus/pkg/membership"
)
// TestHealthExposesPosture: /healthz publishes the node's security posture so a
// monitor (or a peer) can detect a cluster member that is not enforce+ACL+TLS
// (audit 0008 N1). The probe stays unauthenticated.
func TestHealthExposesPosture(t *testing.T) {
dir := t.TempDir()
store, err := membership.Open(filepath.Join(dir, "unibus.db"))
if err != nil {
t.Fatalf("store: %v", err)
}
t.Cleanup(func() { store.Close() })
blobs, _ := blobstore.New(filepath.Join(dir, "blobs"))
srv := membership.NewServer(store, blobs, membership.AuthEnforce)
srv.Posture = membership.Posture{Enforce: true, ACL: true, TLS: true, Cluster: true, Store: "kv"}
ts := httptest.NewServer(srv)
t.Cleanup(ts.Close)
resp, err := http.Get(ts.URL + "/healthz")
if err != nil {
t.Fatalf("get healthz: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("healthz status %d, want 200", resp.StatusCode)
}
body, _ := io.ReadAll(resp.Body)
var got struct {
Status string `json:"status"`
Posture membership.Posture `json:"posture"`
}
if err := json.Unmarshal(body, &got); err != nil {
t.Fatalf("decode healthz %q: %v", string(body), err)
}
if got.Status != "ok" {
t.Fatalf("status = %q, want ok", got.Status)
}
if !got.Posture.Enforce || !got.Posture.ACL || !got.Posture.TLS || !got.Posture.Cluster {
t.Fatalf("posture not surfaced correctly: %+v", got.Posture)
}
if got.Posture.Store != "kv" {
t.Fatalf("posture.store = %q, want kv", got.Posture.Store)
}
}