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:
|
// configuration that would expose the bus without enforced authentication:
|
||||||
//
|
//
|
||||||
// - a non-loopback --bind without --bus-auth enforce (the data plane and
|
// - 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
|
// - --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
|
// 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.
|
// 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",
|
"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)
|
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
|
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
|
// TestBootConfigPolicy is the full table: the golden secure-public config is
|
||||||
// allowed, dev loopback is allowed, and every fail-open shape is refused.
|
// allowed, dev loopback is allowed, and every fail-open shape is refused.
|
||||||
func TestBootConfigPolicy(t *testing.T) {
|
func TestBootConfigPolicy(t *testing.T) {
|
||||||
@@ -41,19 +66,25 @@ func TestBootConfigPolicy(t *testing.T) {
|
|||||||
key string
|
key string
|
||||||
wantErr bool
|
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+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).
|
// Edge: local dev on loopback may stay open (no auth, no TLS).
|
||||||
{"loopback+off", "127.0.0.1", membership.AuthOff, "", "", false},
|
{"loopback+off", "127.0.0.1", membership.AuthOff, "", "", false},
|
||||||
{"loopback-ipv6+off", "::1", membership.AuthOff, "", "", false},
|
{"loopback-ipv6+off", "::1", membership.AuthOff, "", "", false},
|
||||||
{"localhost+off", "localhost", membership.AuthOff, "", "", false},
|
{"localhost+off", "localhost", membership.AuthOff, "", "", false},
|
||||||
{"loopback+soft", "127.0.0.1", membership.AuthSoft, "", "", 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.
|
// Error: public bind without enforce.
|
||||||
{"public+off", "0.0.0.0", membership.AuthOff, "", "", true},
|
{"public+off", "0.0.0.0", membership.AuthOff, "", "", true},
|
||||||
{"public+soft", "0.0.0.0", membership.AuthSoft, "", "", true},
|
{"public+soft", "0.0.0.0", membership.AuthSoft, "", "", true},
|
||||||
{"lan-ip+off", "192.168.1.10", membership.AuthOff, "", "", true},
|
{"lan-ip+off", "192.168.1.10", membership.AuthOff, "", "", true},
|
||||||
{"empty-bind+off", "", 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).
|
// 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+tlscert+off", "127.0.0.1", membership.AuthOff, "s.crt", "", true},
|
||||||
{"loopback+tlskey+soft", "127.0.0.1", membership.AuthSoft, "", "s.key", true},
|
{"loopback+tlskey+soft", "127.0.0.1", membership.AuthSoft, "", "s.key", true},
|
||||||
|
|||||||
Reference in New Issue
Block a user