Merge issue/0005d-tls-guard: require TLS on public bind (audit N4)
This commit is contained in:
@@ -43,9 +43,12 @@ func isLoopbackBind(bind string) bool {
|
||||
// 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
|
||||
// control plane would both accept anyone),
|
||||
// - --tls-cert/--tls-key without --bus-auth enforce (TLS encrypts the channel
|
||||
// but authenticates no one — encrypted access for everybody is still open).
|
||||
// but authenticates no one — encrypted access for everybody is still open), and
|
||||
// - a non-loopback --bind WITHOUT --tls-cert/--tls-key (the control plane would
|
||||
// serve metadata over plaintext HTTP publicly — audit H5 reappearing, the N4
|
||||
// gap the re-audit found: TLS was available but not mandatory).
|
||||
//
|
||||
// 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.
|
||||
@@ -60,6 +63,11 @@ func validateBootConfig(bind string, mode membership.AuthMode, tlsCert, tlsKey s
|
||||
"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)
|
||||
}
|
||||
if !isLoopbackBind(bind) && (tlsCert == "" || tlsKey == "") {
|
||||
return fmt.Errorf(
|
||||
"refusing to start: --bind %q is not loopback but --tls-cert/--tls-key are not both set; a public control plane must serve HTTPS or its metadata (subjects, pubkeys, sealed keys, the social graph) travels in cleartext to a network MITM (audit H5/N4) — provide a CA-signed --tls-cert/--tls-key, or bind 127.0.0.1 for local dev",
|
||||
bind)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,31 @@ func TestAudit_FailOpenTLSWithoutAuth(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestGap_PublicEnforceNoTLS ports the re-auditor's N4 gap: the H2 guard refused
|
||||
// "public without enforce" and "TLS without enforce", but ALLOWED a public bind
|
||||
// with enforce and NO --tls-cert, so the control plane served metadata over
|
||||
// plaintext HTTP publicly (H5 reappearing). The guard now refuses it.
|
||||
func TestGap_PublicEnforceNoTLS(t *testing.T) {
|
||||
// The exact auditor configuration: public bind, enforce on, no TLS cert/key.
|
||||
err := validateBootConfig("0.0.0.0", membership.AuthEnforce, "", "")
|
||||
if err == nil {
|
||||
t.Fatalf("public bind + enforce + NO --tls-cert must be refused: the control plane would serve plaintext HTTP publicly (audit N4)")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "tls-cert") {
|
||||
t.Fatalf("error should point the operator at --tls-cert/--tls-key, got: %v", err)
|
||||
}
|
||||
|
||||
// Golden: the same public+enforce config WITH a cert/key is allowed.
|
||||
if err := validateBootConfig("0.0.0.0", membership.AuthEnforce, "server.crt", "server.key"); err != nil {
|
||||
t.Fatalf("public + enforce + TLS is the intended production config, got: %v", err)
|
||||
}
|
||||
|
||||
// Edge: loopback without TLS stays allowed (local dev is not a public exposure).
|
||||
if err := validateBootConfig("127.0.0.1", membership.AuthOff, "", ""); err != nil {
|
||||
t.Fatalf("loopback dev without TLS must remain allowed, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
@@ -41,19 +66,25 @@ func TestBootConfigPolicy(t *testing.T) {
|
||||
key string
|
||||
wantErr bool
|
||||
}{
|
||||
// Golden: the intended public production config.
|
||||
// Golden: the intended public production config — enforce AND TLS.
|
||||
{"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},
|
||||
// Edge: loopback with full enforce+TLS is also fine.
|
||||
{"loopback+enforce+tls", "127.0.0.1", membership.AuthEnforce, "s.crt", "s.key", 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 (N4): public bind + enforce but NO TLS -> plaintext control plane.
|
||||
{"public+enforce+notls", "0.0.0.0", membership.AuthEnforce, "", "", true},
|
||||
{"public+enforce+certonly", "0.0.0.0", membership.AuthEnforce, "s.crt", "", true},
|
||||
{"public+enforce+keyonly", "0.0.0.0", membership.AuthEnforce, "", "s.key", true},
|
||||
{"lan-ip+enforce+notls", "192.168.1.10", membership.AuthEnforce, "", "", 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},
|
||||
|
||||
Reference in New Issue
Block a user