diff --git a/docs/mcps.md b/docs/mcps.md index 6285c94c245..a1b93355b33 100644 --- a/docs/mcps.md +++ b/docs/mcps.md @@ -194,6 +194,41 @@ tools: X-Custom-Key: "${secrets.CUSTOM_KEY}" ``` +## Network Egress Permissions + +Restrict outbound network access for containerized MCP servers using a per‑tool domain allowlist. Define allowed domains under `mcp.permissions.network.allowed`. + +```yaml +tools: + fetch: + mcp: + container: mcp/fetch + permissions: + network: + allowed: + - "example.com" + allowed: ["fetch"] +``` + +Enforcement in compiled workflows: + +- A [Squid proxy](https://www.squid-cache.org/) is generated and pinned to a dedicated Docker network for each proxy‑enabled MCP server. +- The MCP container is configured with `HTTP_PROXY`/`HTTPS_PROXY` to point at Squid; iptables rules only allow egress to the proxy. +- The proxy is seeded with an `allowed_domains.txt` built from your `allowed` list; requests to other domains are blocked. + +Notes: + +- **Only applies to stdio MCP servers with `container`** - Non‑container stdio and `type: http` servers will cause compilation errors +- Use bare domains without scheme; list each domain you intend to permit. + +### Validation Rules + +The compiler enforces these network permission rules: + +- ❌ **HTTP servers**: `network egress permissions do not apply to remote 'type: http' servers` +- ❌ **Non-container stdio**: `network egress permissions only apply to stdio MCP servers that specify a 'container'` +- ✅ **Container stdio**: Network permissions work correctly + ## Debugging and Troubleshooting ### MCP Server Inspection @@ -255,4 +290,4 @@ Error: Tool 'my_tool' not found ## External Resources - [Model Context Protocol Specification](https://github.com/modelcontextprotocol/specification) -- [GitHub MCP Server](https://github.com/github/github-mcp-server) \ No newline at end of file +- [GitHub MCP Server](https://github.com/github/github-mcp-server) diff --git a/docs/security-notes.md b/docs/security-notes.md index 7d8926d2abe..5a7fe0908e3 100644 --- a/docs/security-notes.md +++ b/docs/security-notes.md @@ -169,7 +169,33 @@ tools: #### Egress Filtering -A critical guardrail is strict control over outbound network connections. Consider using network proxies to enforce allowlists for outbound hosts. +A critical guardrail is strict control over outbound network connections. Agentic Workflows now supports declarative network allowlists for containerized MCP servers. + +Example (domain allowlist): + +```yaml +tools: + fetch: + mcp: + type: stdio + container: mcp/fetch + permissions: + network: + allowed: + - "example.com" + allowed: ["fetch"] +``` + +Enforcement details: + +- Compiler generates a per‑tool Squid proxy and Docker network; MCP egress is forced through the proxy via iptables. +- Only listed domains are reachable; all others are denied at the network layer. +- Applies to `mcp.container` stdio servers. Non‑container stdio and `type: http` servers are not supported and will cause compilation errors. + +Operational guidance: + +- Use bare domains (no scheme). Explicitly list each domain you intend to permit. +- Prefer minimal allowlists; review the compiled `.lock.yml` to verify proxy setup and rules. ### Agent Security and Prompt Injection Defense diff --git a/pkg/workflow/mcp-config.go b/pkg/workflow/mcp-config.go index 7e70b91b34b..c519e685f55 100644 --- a/pkg/workflow/mcp-config.go +++ b/pkg/workflow/mcp-config.go @@ -371,7 +371,7 @@ func ValidateMCPConfigs(tools map[string]any) error { } // Validate MCP configuration requirements (before transformation) - if err := validateMCPRequirements(toolName, mcpConfig); err != nil { + if err := validateMCPRequirements(toolName, mcpConfig, config); err != nil { return err } } @@ -471,7 +471,7 @@ func hasNetworkPermissions(toolConfig map[string]any) (bool, []string) { } // validateMCPRequirements validates the specific requirements for MCP configuration -func validateMCPRequirements(toolName string, mcpConfig map[string]any) error { +func validateMCPRequirements(toolName string, mcpConfig map[string]any, toolConfig map[string]any) error { // Validate 'type' property mcpType, hasType := mcpConfig["type"] if err := validateStringProperty(toolName, "type", mcpType, hasType); err != nil { @@ -489,6 +489,25 @@ func validateMCPRequirements(toolName string, mcpConfig map[string]any) error { return fmt.Errorf("tool '%s' mcp configuration 'type' value must be one of: stdio, http", toolName) } + // Validate network permissions usage first + hasNetPerms, _ := hasNetworkPermissions(toolConfig) + if !hasNetPerms { + // Also check if permissions are nested in the mcp config itself + hasNetPerms, _ = hasNetworkPermissions(map[string]any{"mcp": mcpConfig}) + } + if hasNetPerms { + switch typeStr { + case "http": + return fmt.Errorf("tool '%s' has network permissions configured, but network egress permissions do not apply to remote 'type: http' servers", toolName) + case "stdio": + // Network permissions only apply to stdio servers with container + _, hasContainer := mcpConfig["container"] + if !hasContainer { + return fmt.Errorf("tool '%s' has network permissions configured, but network egress permissions only apply to stdio MCP servers that specify a 'container'", toolName) + } + } + } + // Validate type-specific requirements switch typeStr { case "http": diff --git a/pkg/workflow/mcp_json_test.go b/pkg/workflow/mcp_json_test.go index f83bae836ff..0013ee65b8e 100644 --- a/pkg/workflow/mcp_json_test.go +++ b/pkg/workflow/mcp_json_test.go @@ -320,6 +320,81 @@ func TestValidateMCPConfigs(t *testing.T) { wantErr: true, errMsg: "missing property 'url'", }, + { + name: "network permissions with HTTP type should fail", + tools: map[string]any{ + "httpWithNetPerms": map[string]any{ + "mcp": map[string]any{ + "type": "http", + "url": "https://example.com", + }, + "permissions": map[string]any{ + "network": map[string]any{ + "allowed": []any{"example.com"}, + }, + }, + "allowed": []any{"tool1"}, + }, + }, + wantErr: true, + errMsg: "network egress permissions do not apply to remote 'type: http' servers", + }, + { + name: "network permissions with stdio non-container should fail", + tools: map[string]any{ + "stdioNonContainerWithNetPerms": map[string]any{ + "mcp": map[string]any{ + "type": "stdio", + "command": "python", + }, + "permissions": map[string]any{ + "network": map[string]any{ + "allowed": []any{"example.com"}, + }, + }, + "allowed": []any{"tool1"}, + }, + }, + wantErr: true, + errMsg: "network egress permissions only apply to stdio MCP servers that specify a 'container'", + }, + { + name: "network permissions with stdio container should pass", + tools: map[string]any{ + "stdioContainerWithNetPerms": map[string]any{ + "mcp": map[string]any{ + "type": "stdio", + "container": "mcp/fetch", + }, + "permissions": map[string]any{ + "network": map[string]any{ + "allowed": []any{"example.com"}, + }, + }, + "allowed": []any{"tool1"}, + }, + }, + wantErr: false, + }, + { + name: "network permissions in mcp section with HTTP type should fail", + tools: map[string]any{ + "httpWithMcpNetPerms": map[string]any{ + "mcp": map[string]any{ + "type": "http", + "url": "https://example.com", + "permissions": map[string]any{ + "network": map[string]any{ + "allowed": []any{"example.com"}, + }, + }, + }, + "allowed": []any{"tool1"}, + }, + }, + wantErr: true, + errMsg: "network egress permissions do not apply to remote 'type: http' servers", + }, } for _, tt := range tests {