9b96537aa6
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>
58 lines
1.7 KiB
Go
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)
|
|
}
|
|
}
|