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,50 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/enmanuel/unibus/pkg/membership"
|
||||
)
|
||||
|
||||
// isLoopbackBind reports whether the --bind value keeps the service reachable
|
||||
// only from this host. An empty bind means "all interfaces" (public), and a
|
||||
// hostname we cannot resolve to a loopback literal is treated as public — the
|
||||
// conservative choice, so an unusual bind never silently slips past the guard.
|
||||
func isLoopbackBind(bind string) bool {
|
||||
switch bind {
|
||||
case "localhost":
|
||||
return true
|
||||
case "":
|
||||
return false // empty binds every interface
|
||||
}
|
||||
ip := net.ParseIP(bind)
|
||||
if ip == nil {
|
||||
return false // a hostname we can't classify: assume public
|
||||
}
|
||||
return ip.IsLoopback()
|
||||
}
|
||||
|
||||
// validateBootConfig is the fail-open guard (audit H2). It refuses any startup
|
||||
// configuration that would expose the bus without enforced authentication:
|
||||
//
|
||||
// - a non-loopback --bind without --bus-auth enforce (the data plane and
|
||||
// control plane would both accept anyone), and
|
||||
// - --tls-cert/--tls-key without --bus-auth enforce (TLS encrypts the channel
|
||||
// but authenticates no one — encrypted access for everybody is still open).
|
||||
//
|
||||
// It is a pure function of the parsed flags so the command can fail fast at
|
||||
// startup and tests can assert the policy without booting a server.
|
||||
func validateBootConfig(bind string, mode membership.AuthMode, tlsCert, tlsKey string) error {
|
||||
if !isLoopbackBind(bind) && mode != membership.AuthEnforce {
|
||||
return fmt.Errorf(
|
||||
"refusing to start: --bind %q is not loopback but --bus-auth is %q; a public bind requires --bus-auth enforce (or bind 127.0.0.1 for local dev)",
|
||||
bind, mode)
|
||||
}
|
||||
if (tlsCert != "" || tlsKey != "") && mode != membership.AuthEnforce {
|
||||
return fmt.Errorf(
|
||||
"refusing to start: --tls-cert/--tls-key set but --bus-auth is %q; TLS without enforced auth is fail-open (encrypted channel, no authentication) — set --bus-auth enforce",
|
||||
mode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user