diff --git a/features/cooked-mcp/server.feature.yaml b/features/cooked-mcp/server.feature.yaml index 2158a3498fb2ef5bede3ace78de16b5c91f14c6e..fa91c8e0eb0a06f1b605e4e57133829ec87ff74b 100644 --- a/features/cooked-mcp/server.feature.yaml +++ b/features/cooked-mcp/server.feature.yaml @@ -41,7 +41,10 @@ constraints: 3: Multi-account routing, per-client users, and per-request Cooked credentials are out of scope. SECURITY: requirements: - 1: HTTP transport refuses non-loopback listeners unless MCP HTTP authentication is configured. + 1: + requirement: HTTP transport refuses non-loopback listeners unless MCP HTTP authentication is configured. + deprecated: true + 1-1: HTTP transport accepts any configured listen address without requiring MCP HTTP authentication. 2: HTTP authentication secrets are never logged. 3: HTTP transport supports bearer-token authentication. 4: HTTP bearer tokens can be configured through COOKED_MCP_HTTP_TOKEN. diff --git a/internal/appconfig/config.go b/internal/appconfig/config.go index 734e12bc5dcd20c30490f68a10941a1cbfffd271..aa42049c9d933879c20954e19e5d4344bc544ecf 100644 --- a/internal/appconfig/config.go +++ b/internal/appconfig/config.go @@ -117,9 +117,6 @@ func Load( if err != nil { return Config{}, err } - if err := validateHTTPListenerSafety(httpAddr, httpToken); err != nil { - return Config{}, err - } return Config{ Username: username, @@ -242,21 +239,6 @@ func httpToken(ctx context.Context, resolver *configvalue.Resolver, config *stri return value, nil } -func validateHTTPListenerSafety(addr, token string) error { - host, _, err := net.SplitHostPort(addr) - if err != nil { - return fmt.Errorf("invalid MCP HTTP listen address: %w", err) - } - if isLoopbackHost(host) { - return nil - } - if token != "" { - return nil - } - - return fmt.Errorf("MCP HTTP token is required for non-loopback HTTP listen addresses") -} - func isLoopbackHost(host string) bool { if strings.EqualFold(host, "localhost") { return true diff --git a/internal/appconfig/config_test.go b/internal/appconfig/config_test.go index 9d46e9d6004df9bd21eb35b07fbf61cc96cd6a3e..79b5eee9ea26d361d9e861151840d78a66b4b612 100644 --- a/internal/appconfig/config_test.go +++ b/internal/appconfig/config_test.go @@ -233,8 +233,8 @@ http_token = "" } } -// server.SECURITY.1 -func TestLoadACIDServerSecurity1AllowsLoopbackHTTPAddrWithoutToken(t *testing.T) { +// server.SECURITY.1-1 +func TestLoadACIDServerSecurity1_1AllowsLoopbackHTTPAddrWithoutToken(t *testing.T) { tests := []struct { name string addr string @@ -269,69 +269,65 @@ http_addr = "`+tt.addr+`" } } -// server.SECURITY.1 -func TestLoadACIDServerSecurity1RejectsNonLoopbackHTTPAddrWithoutToken(t *testing.T) { - configPath := filepath.Join(t.TempDir(), "config.toml") - writeConfig(t, configPath, `[user] -name = "toml-user" -password = "toml-password" - -[mcp] -http_addr = "0.0.0.0:8123" -`) - - _, err := Load(context.Background(), configPath, configvalue.NewResolver(), LoadOptions{}) - if err == nil { - t.Fatal("Load() error = nil, want non-loopback HTTP token error") - } - if !strings.Contains(err.Error(), "MCP HTTP token is required") { - t.Fatalf("Load() error = %v, want HTTP token required error", err) +// server.SECURITY.1-1 +func TestLoadACIDServerSecurity1_1AllowsHTTPAddrWithoutToken(t *testing.T) { + tests := []struct { + name string + addr string + }{ + {name: "all interfaces", addr: "0.0.0.0:8123"}, + {name: "LAN", addr: "192.168.1.10:8123"}, + {name: "Tailscale", addr: "100.64.0.1:8123"}, + {name: "hostname-like LAN", addr: "192.168.1.blah:8123"}, } -} -// server.SECURITY.1 -func TestLoadACIDServerSecurity1AllowsNonLoopbackHTTPAddrWithToken(t *testing.T) { - configPath := filepath.Join(t.TempDir(), "config.toml") - writeConfig(t, configPath, `[user] + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + configPath := filepath.Join(t.TempDir(), "config.toml") + writeConfig(t, configPath, `[user] name = "toml-user" password = "toml-password" [mcp] -http_addr = "0.0.0.0:8123" -http_token = "secret-token" +http_addr = "`+tt.addr+`" `) - got, err := Load(context.Background(), configPath, configvalue.NewResolver(), LoadOptions{}) - if err != nil { - t.Fatalf("Load() error = %v", err) - } - if got.HTTPAddr != "0.0.0.0:8123" { - t.Fatalf("HTTPAddr = %q, want non-loopback address", got.HTTPAddr) - } - if got.HTTPToken != "secret-token" { - t.Fatal("HTTPToken was not resolved") + got, err := Load(context.Background(), configPath, configvalue.NewResolver(), LoadOptions{}) + if err != nil { + t.Fatalf("Load() error = %v", err) + } + if got.HTTPAddr != tt.addr { + t.Fatalf("HTTPAddr = %q, want %q", got.HTTPAddr, tt.addr) + } + if got.HTTPToken != "" { + t.Fatalf("HTTPToken = %q, want empty", got.HTTPToken) + } + }) } } -// server.SECURITY.1 -func TestLoadACIDServerSecurity1RejectsNonLoopbackHTTPAddrOverrideWithoutToken(t *testing.T) { +// server.SECURITY.1-1 +func TestLoadACIDServerSecurity1_1AllowsHTTPAddrOverrideWithoutToken(t *testing.T) { configPath := filepath.Join(t.TempDir(), "config.toml") writeConfig(t, configPath, `[user] name = "toml-user" password = "toml-password" `) - _, err := Load( + got, err := Load( context.Background(), configPath, configvalue.NewResolver(), LoadOptions{HTTPAddr: "0.0.0.0:8123"}, ) - if err == nil { - t.Fatal("Load() error = nil, want non-loopback HTTP token error") + if err != nil { + t.Fatalf("Load() error = %v", err) + } + if got.HTTPAddr != "0.0.0.0:8123" { + t.Fatalf("HTTPAddr = %q, want override", got.HTTPAddr) } - if !strings.Contains(err.Error(), "MCP HTTP token is required") { - t.Fatalf("Load() error = %v, want HTTP token required error", err) + if got.HTTPToken != "" { + t.Fatalf("HTTPToken = %q, want empty", got.HTTPToken) } }