feat(membershipd): refuse fail-open startup configs
Audit H2 (Alto). The binary defaulted to --bus-auth off, the NATS nkey authenticator only turned on under enforce, and TLS was an independent flag. Booting --bind 0.0.0.0 --tls-cert … without --bus-auth enforce left both planes open while looking secure. validateBootConfig is a pure guard, called right after flag parsing, that log.Fatals on two insecure shapes: - a non-loopback --bind without --bus-auth enforce, and - --tls-cert/--tls-key without --bus-auth enforce. An insecure public startup is now impossible (the process exits), so a fail-open data plane never comes up for an unregistered client to reach. TestAudit_FailOpenTLSWithoutAuth plus a full policy table cover golden (public+enforce, dev loopback) and every refused shape. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/enmanuel/unibus/pkg/membership"
|
||||
)
|
||||
|
||||
// TestAudit_FailOpenTLSWithoutAuth ports the auditor's H2 vector. Before the
|
||||
// guard, booting with TLS on but the authenticator off ("--bind 0.0.0.0
|
||||
// --tls-cert … " without enforce) produced an encrypted data plane that an
|
||||
// unregistered, nkey-less client could still connect to — a fail-open config
|
||||
// wearing the appearance of security. validateBootConfig now refuses it, so the
|
||||
// insecure server never starts (the client therefore has nothing to connect to).
|
||||
func TestAudit_FailOpenTLSWithoutAuth(t *testing.T) {
|
||||
// The exact auditor configuration: public bind, TLS provided, auth off.
|
||||
err := validateBootConfig("0.0.0.0", membership.AuthOff, "server.crt", "server.key")
|
||||
if err == nil {
|
||||
t.Fatalf("TLS without enforce on a public bind must be refused at startup")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "enforce") {
|
||||
t.Fatalf("error should point the operator at --bus-auth enforce, got: %v", err)
|
||||
}
|
||||
|
||||
// And TLS without enforce is rejected even on loopback: TLS implies a
|
||||
// security posture, so authenticating no one is always a misconfiguration.
|
||||
if err := validateBootConfig("127.0.0.1", membership.AuthOff, "server.crt", "server.key"); err == nil {
|
||||
t.Fatalf("TLS flags without enforce must be refused regardless of bind")
|
||||
}
|
||||
}
|
||||
|
||||
// TestBootConfigPolicy is the full table: the golden secure-public config is
|
||||
// allowed, dev loopback is allowed, and every fail-open shape is refused.
|
||||
func TestBootConfigPolicy(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
bind string
|
||||
mode membership.AuthMode
|
||||
cert string
|
||||
key string
|
||||
wantErr bool
|
||||
}{
|
||||
// Golden: the intended public production config.
|
||||
{"public+enforce+tls", "0.0.0.0", membership.AuthEnforce, "s.crt", "s.key", false},
|
||||
{"public+enforce+notls", "0.0.0.0", membership.AuthEnforce, "", "", false},
|
||||
// Edge: local dev on loopback may stay open (no auth, no TLS).
|
||||
{"loopback+off", "127.0.0.1", membership.AuthOff, "", "", false},
|
||||
{"loopback-ipv6+off", "::1", membership.AuthOff, "", "", false},
|
||||
{"localhost+off", "localhost", membership.AuthOff, "", "", false},
|
||||
{"loopback+soft", "127.0.0.1", membership.AuthSoft, "", "", false},
|
||||
// Error: public bind without enforce.
|
||||
{"public+off", "0.0.0.0", membership.AuthOff, "", "", true},
|
||||
{"public+soft", "0.0.0.0", membership.AuthSoft, "", "", true},
|
||||
{"lan-ip+off", "192.168.1.10", membership.AuthOff, "", "", true},
|
||||
{"empty-bind+off", "", membership.AuthOff, "", "", true},
|
||||
// Error: TLS flags without enforce (cert or key alone is enough to trip it).
|
||||
{"loopback+tlscert+off", "127.0.0.1", membership.AuthOff, "s.crt", "", true},
|
||||
{"loopback+tlskey+soft", "127.0.0.1", membership.AuthSoft, "", "s.key", true},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
err := validateBootConfig(c.bind, c.mode, c.cert, c.key)
|
||||
if c.wantErr && err == nil {
|
||||
t.Fatalf("config %+v should be refused", c)
|
||||
}
|
||||
if !c.wantErr && err != nil {
|
||||
t.Fatalf("config %+v should be allowed, got: %v", c, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user